573 lines
19 KiB
Swift
573 lines
19 KiB
Swift
/// Describes a database column.
|
|
///
|
|
/// You get instances of `ColumnDefinition` when you create or alter a database
|
|
/// tables. For example:
|
|
///
|
|
/// ```swift
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("name", .text) // ColumnDefinition
|
|
/// }
|
|
///
|
|
/// try db.alter(table: "player") { t in
|
|
/// t.add(column: "score", .integer) // ColumnDefinition
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// See ``TableDefinition/column(_:_:)`` and ``TableAlteration/add(column:_:)``.
|
|
///
|
|
/// Related SQLite documentation:
|
|
///
|
|
/// - <https://www.sqlite.org/lang_createtable.html>
|
|
/// - <https://www.sqlite.org/lang_altertable.html>
|
|
///
|
|
/// ## Topics
|
|
///
|
|
/// ### Foreign Keys
|
|
///
|
|
/// - ``references(_:column:onDelete:onUpdate:deferred:)``
|
|
///
|
|
/// ### Indexes
|
|
///
|
|
/// - ``indexed()``
|
|
/// - ``unique(onConflict:)``
|
|
///
|
|
/// ### Default value
|
|
///
|
|
/// - ``defaults(to:)``
|
|
/// - ``defaults(sql:)``
|
|
///
|
|
/// ### Collations
|
|
///
|
|
/// - ``collate(_:)-4dljx``
|
|
/// - ``collate(_:)-9ywza``
|
|
///
|
|
/// ### Generated Columns
|
|
///
|
|
/// - ``generatedAs(_:_:)``
|
|
/// - ``generatedAs(sql:_:)``
|
|
/// - ``GeneratedColumnQualification``
|
|
///
|
|
/// ### Other Constraints
|
|
///
|
|
/// - ``check(_:)``
|
|
/// - ``check(sql:)``
|
|
/// - ``notNull(onConflict:)``
|
|
///
|
|
/// ### Sunsetted Methods
|
|
///
|
|
/// Those are legacy interfaces that are preserved for backwards compatibility.
|
|
/// Their use is not recommended.
|
|
///
|
|
/// - ``primaryKey(onConflict:autoincrement:)``
|
|
public final class ColumnDefinition {
|
|
enum Indexing {
|
|
case index
|
|
case unique(Database.ConflictResolution)
|
|
}
|
|
|
|
struct ForeignKeyConstraint {
|
|
var destinationTable: String
|
|
var destinationColumn: String?
|
|
var deleteAction: Database.ForeignKeyAction?
|
|
var updateAction: Database.ForeignKeyAction?
|
|
var isDeferred: Bool
|
|
}
|
|
|
|
/// The kind of a generated column.
|
|
///
|
|
/// Related SQLite documentation: <https://sqlite.org/gencol.html#virtual_versus_stored_columns>
|
|
public enum GeneratedColumnQualification: Sendable {
|
|
/// A `VIRTUAL` generated column.
|
|
case virtual
|
|
/// A `STORED` generated column.
|
|
case stored
|
|
}
|
|
|
|
struct GeneratedColumnConstraint {
|
|
var expression: SQLExpression
|
|
var qualification: GeneratedColumnQualification
|
|
}
|
|
|
|
let name: String
|
|
let type: Database.ColumnType?
|
|
var primaryKey: (conflictResolution: Database.ConflictResolution?, autoincrement: Bool)?
|
|
var indexing: Indexing?
|
|
var notNullConflictResolution: Database.ConflictResolution?
|
|
var checkConstraints: [SQLExpression] = []
|
|
var foreignKeyConstraints: [ForeignKeyConstraint] = []
|
|
var defaultExpression: SQLExpression?
|
|
var collationName: String?
|
|
var generatedColumnConstraint: GeneratedColumnConstraint?
|
|
|
|
init(name: String, type: Database.ColumnType?) {
|
|
self.name = name
|
|
self.type = type
|
|
}
|
|
|
|
/// Adds a primary key constraint.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // id TEXT NOT NULL PRIMARY KEY
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.primaryKey("id", .text)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// - important: Make sure you add a not null constraint on your primary key
|
|
/// column, as in the above example, or SQLite will allow null values.
|
|
/// See <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
|
|
/// for more information.
|
|
///
|
|
/// - warning: This is a legacy interface that is preserved for backwards
|
|
/// compatibility. Use of this interface is not recommended: prefer
|
|
/// ``TableDefinition/primaryKey(_:_:onConflict:)``
|
|
/// instead.
|
|
///
|
|
/// - parameters:
|
|
/// - conflictResolution: An optional ``Database/ConflictResolution``.
|
|
/// - autoincrement: If true, the primary key is autoincremented.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func primaryKey(
|
|
onConflict conflictResolution: Database.ConflictResolution? = nil,
|
|
autoincrement: Bool = false)
|
|
-> Self
|
|
{
|
|
primaryKey = (conflictResolution: conflictResolution, autoincrement: autoincrement)
|
|
return self
|
|
}
|
|
|
|
/// Adds a not null constraint.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // name TEXT NOT NULL
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("name", .text).notNull()
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#notnullconst>
|
|
///
|
|
/// - parameter conflictResolution: An optional ``Database/ConflictResolution``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func notNull(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self {
|
|
notNullConflictResolution = conflictResolution ?? .abort
|
|
return self
|
|
}
|
|
|
|
/// Adds a unique constraint.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // email TEXT UNIQUE
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("email", .text).unique()
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#uniqueconst>
|
|
///
|
|
/// - parameter conflictResolution: An optional ``Database/ConflictResolution``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func unique(onConflict conflictResolution: Database.ConflictResolution? = nil) -> Self {
|
|
indexing = .unique(conflictResolution ?? .abort)
|
|
return self
|
|
}
|
|
|
|
/// Adds an index.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(email TEXT);
|
|
/// // CREATE INDEX player_on_email ON player(email);
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("email", .text).indexed()
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// The name of the created index is `<table>_on_<column>`, where `table`
|
|
/// and `column` are the names of the table and the column. See the
|
|
/// example above.
|
|
///
|
|
/// See also ``unique(onConflict:)``.
|
|
///
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func indexed() -> Self {
|
|
if case .none = indexing {
|
|
self.indexing = .index
|
|
}
|
|
return self
|
|
}
|
|
|
|
/// Adds a check constraint.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // name TEXT CHECK (LENGTH(name) > 0)
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("name", .text).check { length($0) > 0 }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
|
|
///
|
|
/// - parameter condition: A closure whose argument is a ``Column`` that
|
|
/// represents the defined column, and returns the expression to check.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func check(_ condition: (Column) -> any SQLExpressible) -> Self {
|
|
checkConstraints.append(condition(Column(name)).sqlExpression)
|
|
return self
|
|
}
|
|
|
|
/// Adds a check constraint.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // name TEXT CHECK (LENGTH(name) > 0)
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("name", .text).check(sql: "LENGTH(name) > 0")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#ckconst>
|
|
///
|
|
/// - parameter sql: An SQL snippet.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func check(sql: String) -> Self {
|
|
checkConstraints.append(SQL(sql: sql).sqlExpression)
|
|
return self
|
|
}
|
|
|
|
/// Defines the default value.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // email TEXT DEFAULT 'Anonymous'
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("name", .text).defaults(to: "Anonymous")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#dfltval>
|
|
///
|
|
/// - parameter value: A ``DatabaseValueConvertible`` value.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func defaults(to value: some DatabaseValueConvertible) -> Self {
|
|
defaultExpression = value.sqlExpression
|
|
return self
|
|
}
|
|
|
|
/// Defines the default value.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // creationDate DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("creationDate", .DateTime).defaults(sql: "CURRENT_TIMESTAMP")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#dfltval>
|
|
///
|
|
/// - parameter sql: An SQL snippet.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func defaults(sql: String) -> Self {
|
|
defaultExpression = SQL(sql: sql).sqlExpression
|
|
return self
|
|
}
|
|
|
|
/// Defines the default collation.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // email TEXT COLLATE NOCASE
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("email", .text).collate(.nocase)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#collation>
|
|
///
|
|
/// - parameter collation: A ``Database/CollationName``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func collate(_ collation: Database.CollationName) -> Self {
|
|
collationName = collation.rawValue
|
|
return self
|
|
}
|
|
|
|
/// Defines the default collation.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// try db.create(table: "player") { t in
|
|
/// t.column("name", .text).collate(.localizedCaseInsensitiveCompare)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#collation>
|
|
///
|
|
/// - parameter collation: A ``DatabaseCollation``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func collate(_ collation: DatabaseCollation) -> Self {
|
|
collationName = collation.name
|
|
return self
|
|
}
|
|
|
|
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
|
/// Defines the column as a generated column.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
/// // score INTEGER NOT NULL,
|
|
/// // bonus INTEGER NOT NULL,
|
|
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.autoIncrementedPrimaryKey("id")
|
|
/// t.column("score", .integer).notNull()
|
|
/// t.column("bonus", .integer).notNull()
|
|
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
|
|
///
|
|
/// - parameters:
|
|
/// - sql: An SQL expression.
|
|
/// - qualification: The generated column's qualification, which
|
|
/// defaults to ``GeneratedColumnQualification/virtual``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func generatedAs(
|
|
sql: String,
|
|
_ qualification: GeneratedColumnQualification = .virtual)
|
|
-> Self
|
|
{
|
|
let expression = SQL(sql: sql).sqlExpression
|
|
generatedColumnConstraint = GeneratedColumnConstraint(
|
|
expression: expression,
|
|
qualification: qualification)
|
|
return self
|
|
}
|
|
|
|
/// Defines the column as a generated column.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
/// // score INTEGER NOT NULL,
|
|
/// // bonus INTEGER NOT NULL,
|
|
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.autoIncrementedPrimaryKey("id")
|
|
/// t.column("score", .integer).notNull()
|
|
/// t.column("bonus", .integer).notNull()
|
|
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
|
|
///
|
|
/// - parameters:
|
|
/// - expression: The generated expression.
|
|
/// - qualification: The generated column's qualification, which
|
|
/// defaults to ``GeneratedColumnQualification/virtual``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func generatedAs(
|
|
_ expression: some SQLExpressible,
|
|
_ qualification: GeneratedColumnQualification = .virtual)
|
|
-> Self
|
|
{
|
|
generatedColumnConstraint = GeneratedColumnConstraint(
|
|
expression: expression.sqlExpression,
|
|
qualification: qualification)
|
|
return self
|
|
}
|
|
#else
|
|
/// Defines the column as a generated column.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
/// // score INTEGER NOT NULL,
|
|
/// // bonus INTEGER NOT NULL,
|
|
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.autoIncrementedPrimaryKey("id")
|
|
/// t.column("score", .integer).notNull()
|
|
/// t.column("bonus", .integer).notNull()
|
|
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
|
|
///
|
|
/// - parameters:
|
|
/// - sql: An SQL expression.
|
|
/// - qualification: The generated column's qualification, which
|
|
/// defaults to ``GeneratedColumnQualification/virtual``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ (3.31 actually)
|
|
@discardableResult
|
|
public func generatedAs(
|
|
sql: String,
|
|
_ qualification: GeneratedColumnQualification = .virtual)
|
|
-> Self
|
|
{
|
|
let expression = SQL(sql: sql).sqlExpression
|
|
generatedColumnConstraint = GeneratedColumnConstraint(
|
|
expression: expression,
|
|
qualification: qualification)
|
|
return self
|
|
}
|
|
|
|
/// Defines the column as a generated column.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE player(
|
|
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
/// // score INTEGER NOT NULL,
|
|
/// // bonus INTEGER NOT NULL,
|
|
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) VIRTUAL
|
|
/// // )
|
|
/// try db.create(table: "player") { t in
|
|
/// t.autoIncrementedPrimaryKey("id")
|
|
/// t.column("score", .integer).notNull()
|
|
/// t.column("bonus", .integer).notNull()
|
|
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
|
|
///
|
|
/// - parameters:
|
|
/// - expression: The generated expression.
|
|
/// - qualification: The generated column's qualification, which
|
|
/// defaults to ``GeneratedColumnQualification/virtual``.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+ (3.31 actually)
|
|
@discardableResult
|
|
public func generatedAs(
|
|
_ expression: some SQLExpressible,
|
|
_ qualification: GeneratedColumnQualification = .virtual)
|
|
-> Self
|
|
{
|
|
generatedColumnConstraint = GeneratedColumnConstraint(
|
|
expression: expression.sqlExpression,
|
|
qualification: qualification)
|
|
return self
|
|
}
|
|
#endif
|
|
|
|
/// Adds a foreign key constraint.
|
|
///
|
|
/// For example:
|
|
///
|
|
/// ```swift
|
|
/// // CREATE TABLE book(
|
|
/// // authorId INTEGER REFERENCES author(id) ON DELETE CASCADE
|
|
/// // )
|
|
/// try db.create(table: "book") { t in
|
|
/// t.column("authorId", .integer).references("author", onDelete: .cascade)
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>
|
|
///
|
|
/// - parameters:
|
|
/// - table: The referenced table.
|
|
/// - column: The referenced column in the referenced table. If not
|
|
/// specified, the column of the primary key of the referenced table
|
|
/// is used.
|
|
/// - deleteAction: Optional action when the referenced row is deleted.
|
|
/// - updateAction: Optional action when the referenced row is updated.
|
|
/// - isDeferred: A boolean value indicating whether the foreign key
|
|
/// constraint is deferred.
|
|
/// See <https://www.sqlite.org/foreignkeys.html#fk_deferred>.
|
|
/// - returns: `self` so that you can further refine the column definition.
|
|
@discardableResult
|
|
public func references(
|
|
_ table: String,
|
|
column: String? = nil,
|
|
onDelete deleteAction: Database.ForeignKeyAction? = nil,
|
|
onUpdate updateAction: Database.ForeignKeyAction? = nil,
|
|
deferred isDeferred: Bool = false) -> Self
|
|
{
|
|
foreignKeyConstraints.append(ForeignKeyConstraint(
|
|
destinationTable: table,
|
|
destinationColumn: column,
|
|
deleteAction: deleteAction,
|
|
updateAction: updateAction,
|
|
isDeferred: isDeferred))
|
|
return self
|
|
}
|
|
|
|
func indexDefinition(in table: String, options: IndexOptions = []) -> IndexDefinition? {
|
|
switch indexing {
|
|
case .none: return nil
|
|
case .unique: return nil
|
|
case .index:
|
|
return IndexDefinition(
|
|
name: "\(table)_on_\(name)",
|
|
table: table,
|
|
expressions: [.column(name)],
|
|
options: options,
|
|
condition: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Explicit non-conformance to Sendable: `ColumnDefinition` is a mutable
|
|
// class and there is no known reason for making it thread-safe.
|
|
@available(*, unavailable)
|
|
extension ColumnDefinition: Sendable { }
|