add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1,572 @@
/// 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 { }
@@ -0,0 +1,694 @@
extension Database {
// MARK: - Database Schema
/// Creates a database table.
///
/// For example:
///
/// ```swift
/// try db.create(table: "place") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("favorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
/// t.column("latitude", .double).notNull()
/// }
/// ```
///
/// Related SQLite documentation:
/// - <https://www.sqlite.org/lang_createtable.html>
/// - <https://www.sqlite.org/withoutrowid.html>
///
/// - warning: This is a legacy interface that is preserved for backwards
/// compatibility. Use of this interface is not recommended: prefer
/// ``create(table:options:body:)`` instead.
///
/// - parameters:
/// - name: The table name.
/// - temporary: If true, creates a temporary table.
/// - ifNotExists: If false (the default), an error is thrown if the
/// table already exists. Otherwise, the table is created unless it
/// already exists.
/// - withoutRowID: If true, uses WITHOUT ROWID optimization.
/// - body: A closure that defines table columns and constraints.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
@_disfavoredOverload
public func create(
table name: String,
temporary: Bool = false,
ifNotExists: Bool = false,
withoutRowID: Bool = false,
body: (TableDefinition) throws -> Void)
throws
{
var options: TableOptions = []
if temporary { options.insert(.temporary) }
if ifNotExists { options.insert(.ifNotExists) }
if withoutRowID { options.insert(.withoutRowID) }
try create(table: name, options: options, body: body)
}
/// Creates a database table.
///
/// ### Reference documentation
///
/// SQLite has many reference documents about table creation. They are a
/// great learning material:
///
/// - [CREATE TABLE](https://www.sqlite.org/lang_createtable.html)
/// - [Datatypes In SQLite](https://www.sqlite.org/datatype3.html)
/// - [SQLite Foreign Key Support](https://www.sqlite.org/foreignkeys.html)
/// - [The ON CONFLICT Clause](https://www.sqlite.org/lang_conflict.html)
/// - [Rowid Tables](https://www.sqlite.org/rowidtable.html)
/// - [The WITHOUT ROWID Optimization](https://www.sqlite.org/withoutrowid.html)
/// - [STRICT Tables](https://www.sqlite.org/stricttables.html)
///
/// ### Usage
///
/// ```swift
/// // CREATE TABLE place (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // title TEXT,
/// // isFavorite BOOLEAN NOT NULL DEFAULT 0,
/// // latitude DOUBLE NOT NULL,
/// // longitude DOUBLE NOT NULL
/// // )
/// try db.create(table: "place") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("isFavorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
/// t.column("latitude", .double).notNull()
/// }
/// ```
///
/// ### Configure table creation
///
/// Use the `options` parameter to configure table creation
/// (see ``TableOptions``):
///
/// ```swift
/// // CREATE TABLE player ( ... )
/// try db.create(table: "player") { t in ... }
///
/// // CREATE TEMPORARY TABLE player IF NOT EXISTS (
/// try db.create(table: "player", options: [.temporary, .ifNotExists]) { t in ... }
/// ```
///
/// ### Add columns
///
/// Add columns with their name and eventual type (`text`, `integer`,
/// `double`, `real`, `numeric`, `boolean`, `blob`, `date`, `datetime`
/// and `any`) - see ``Database/ColumnType``:
///
/// ```swift
/// // CREATE TABLE example (
/// // a,
/// // name TEXT,
/// // creationDate DATETIME,
/// try db.create(table: "example") { t in
/// t.column("a")
/// t.column("name", .text)
/// t.column("creationDate", .datetime)
/// ```
///
/// The `column()` method returns a ``ColumnDefinition`` that you can
/// further configure:
///
/// ### Not null constraints, default values
///
/// ```swift
/// // email TEXT NOT NULL,
/// t.column("email", .text).notNull()
///
/// // name TEXT DEFAULT 'O''Reilly',
/// t.column("name", .text).defaults(to: "O'Reilly")
///
/// // flag BOOLEAN NOT NULL DEFAULT 0,
/// t.column("flag", .boolean).notNull().defaults(to: false)
///
/// // creationDate DATETIME DEFAULT CURRENT_TIMESTAMP,
/// t.column("creationDate", .datetime).defaults(sql: "CURRENT_TIMESTAMP")
/// ```
///
/// ### Primary, unique, and foreign keys
///
/// Use an individual column as **primary**, **unique**, or **foreign key**.
/// When defining a foreign key, the referenced column is the primary key of
/// the referenced table (unless you specify otherwise):
///
/// ```swift
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// t.autoIncrementedPrimaryKey("id")
///
/// // uuid TEXT NOT NULL PRIMARY KEY,
/// t.primaryKey("uuid", .text)
///
/// // email TEXT UNIQUE,
/// t.column("email", .text)
/// .unique()
///
/// // countryCode TEXT REFERENCES country(code) ON DELETE CASCADE,
/// t.column("countryCode", .text)
/// .references("country", onDelete: .cascade)
/// ```
///
/// Primary, unique and foreign keys can also be added on several columns:
///
/// ```swift
/// // a INTEGER NOT NULL,
/// // b TEXT NOT NULL,
/// // PRIMARY KEY (a, b)
/// t.primaryKey {
/// t.column("a", .integer)
/// t.column("b", .text)
/// }
///
/// // a INTEGER NOT NULL,
/// // b TEXT NOT NULL,
/// // PRIMARY KEY (a, b)
/// t.column("a", .integer).notNull()
/// t.column("b", .text).notNull()
/// t.primaryKey(["a", "b"])
///
/// // a INTEGER,
/// // b TEXT,
/// // UNIQUE (a, b) ON CONFLICT REPLACE
/// t.column("a", .integer)
/// t.column("b", .text)
/// t.uniqueKey(["a", "b"], onConflict: .replace)
///
/// // a INTEGER,
/// // b TEXT,
/// // FOREIGN KEY (a, b) REFERENCES parents(c, d)
/// t.column("a", .integer)
/// t.column("b", .text)
/// t.foreignKey(["a", "b"], references: "parents")
/// ```
///
/// > Tip: when you need an integer primary key that automatically generates
/// unique values, it is recommended that you use the
/// ``TableDefinition/autoIncrementedPrimaryKey(_:onConflict:)`` method:
/// >
/// > ```swift
/// > try db.create(table: "example") { t in
/// > t.autoIncrementedPrimaryKey("id")
/// > ...
/// > }
/// > ```
/// >
/// > The reason for this recommendation is that auto-incremented primary
/// > keys forbid the reuse of ids. This prevents your app or
/// > <doc:DatabaseObservation> to think that a row was updated, when it was
/// > actually deleted and replaced. Depending on your application needs,
/// > this may be acceptable. But usually it is not.
///
/// ### Indexed columns
///
/// ```swift
/// t.column("score", .integer).indexed()
/// ```
///
/// For extra index options, see ``create(indexOn:columns:options:condition:)``.
///
/// ### Generated columns
///
/// See [Generated columns](https://sqlite.org/gencol.html) for
/// more information:
///
/// ```swift
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
/// ```
///
/// ### Integrity checks
///
/// SQLite will only let conforming rows in:
///
/// ```swift
/// // name TEXT CHECK (LENGTH(name) > 0)
/// t.column("name", .text).check { length($0) > 0 }
///
/// // score INTEGER CHECK (score > 0)
/// t.column("score", .integer).check(sql: "score > 0")
///
/// // CHECK (a + b < 10),
/// t.check(Column("a") + Column("b") < 10)
///
/// // CHECK (a + b < 10)
/// t.check(sql: "a + b < 10")
/// ```
///
/// ### Raw SQL columns and constraints
///
/// Columns and constraints can be defined with raw sql:
///
/// ```swift
/// t.column(sql: "name TEXT")
/// t.constraint(sql: "CHECK (a + b < 10)")
/// ```
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// let defaultName = "O'Reilly"
/// t.column(literal: "name TEXT DEFAULT \(defaultName)")
///
/// let forbiddenName = "admin"
/// t.constraint(literal: "CHECK (name <> \(forbiddenName))")
/// ```
///
/// - parameters:
/// - name: The table name.
/// - options: Table creation options.
/// - body: A closure that defines table columns and constraints.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
table name: String,
options: TableOptions = [],
body: (TableDefinition) throws -> Void)
throws
{
let table = TableDefinition(
name: name,
options: options)
try body(table)
let generator = try SQLTableGenerator(self, table: table)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Renames a database table.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func rename(table name: String, to newName: String) throws {
try execute(sql: "ALTER TABLE \(name.quotedDatabaseIdentifier) RENAME TO \(newName.quotedDatabaseIdentifier)")
}
/// Modifies a database table.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.add(column: "url", .text)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameters:
/// - name: The table name.
/// - body: A closure that defines table alterations.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func alter(table name: String, body: (TableAlteration) -> Void) throws {
let alteration = TableAlteration(name: name)
body(alteration)
let generator = SQLTableAlterationGenerator(alteration)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Deletes a database table.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_droptable.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(table name: String) throws {
try execute(sql: "DROP TABLE \(name.quotedDatabaseIdentifier)")
}
/// Creates a database view.
///
/// You can create a view with an ``SQLRequest``:
///
/// ```swift
/// // CREATE VIEW hero AS SELECT * FROM player WHERE isHero == 1
/// try db.create(view: "hero", as: SQLRequest(literal: """
/// SELECT * FROM player WHERE isHero == 1
/// """)
/// ```
///
/// You can also create a view with a ``QueryInterfaceRequest``:
///
/// ```swift
/// // CREATE VIEW hero AS SELECT * FROM player WHERE isHero == 1
/// try db.create(
/// view: "hero",
/// as: Player.filter(Column("isHero") == true))
/// ```
///
/// When creating views in <doc:Migrations>, it is not recommended to
/// use record types defined in the application. Instead of the `Player`
/// record type, prefer `Table("player")`:
///
/// ```swift
/// // RECOMMENDED IN MIGRATIONS
/// try db.create(
/// view: "hero",
/// as: Table("player").filter(Column("isHero") == true))
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createview.html>
///
/// - parameters:
/// - view: The view name.
/// - options: View creation options.
/// - columns: The columns of the view. If nil, the columns are the
/// columns of the request.
/// - request: The request that feeds the view.
public func create(
view name: String,
options: ViewOptions = [],
columns: [String]? = nil,
as request: SQLSubqueryable)
throws {
var literal: SQL = "CREATE "
if options.contains(.temporary) {
literal += "TEMPORARY "
}
literal += "VIEW "
if options.contains(.ifNotExists) {
literal += "IF NOT EXISTS "
}
literal += "\(identifier: name) "
if let columns {
literal += "("
literal += columns.map { "\(identifier: $0)" }.joined(separator: ", ")
literal += ") "
}
literal += "AS \(request)"
// CREATE VIEW does not support arguments, so make sure we use
// literal values.
let context = SQLGenerationContext(self, argumentsSink: .literalValues)
let sql = try literal.sql(context)
try execute(sql: sql)
}
/// Creates a database view.
///
/// For example:
///
/// ```swift
/// // CREATE VIEW hero AS SELECT * FROM player WHERE isHero == 1
/// try db.create(view: "hero", asLiteral: """
/// SELECT * FROM player WHERE isHero == 1
/// """)
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createview.html>
///
/// - parameters:
/// - view: The view name.
/// - options: View creation options.
/// - columns: The columns of the view. If nil, the columns are the
/// columns of the request.
/// - sqlLiteral: An `SQL` literal.
public func create(
view name: String,
options: ViewOptions = [],
columns: [String]? = nil,
asLiteral sqlLiteral: SQL)
throws {
try create(view: name, options: options, columns: columns, as: SQLRequest(literal: sqlLiteral))
}
/// Deletes a database view.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_dropview.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(view name: String) throws {
try execute(sql: "DROP VIEW \(name.quotedDatabaseIdentifier)")
}
/// Creates an index on the specified table and columns.
///
/// For example:
///
/// ```swift
/// // CREATE INDEX index_player_on_email ON player(email)
/// try db.create(index: "index_player_on_email", on: "player", columns: ["email"])
/// ```
///
/// SQLite can also index expressions (<https://www.sqlite.org/expridx.html>)
/// and use specific collations. To create such an index, use
/// ``create(index:on:expressions:options:condition:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createindex.html>
///
/// - warning: This is a legacy interface that is preserved for backwards
/// compatibility. Use of this interface is not recommended: prefer
/// ``create(indexOn:columns:options:condition:)`` instead.
///
/// - parameters:
/// - name: The index name.
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - unique: If true, creates a unique index.
/// - ifNotExists: If true, no error is thrown if index already exists.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
@_disfavoredOverload
public func create(
index name: String,
on table: String,
columns: [String],
unique: Bool = false,
ifNotExists: Bool = false,
condition: (any SQLExpressible)? = nil)
throws
{
var options: IndexOptions = []
if ifNotExists { options.insert(.ifNotExists) }
if unique { options.insert(.unique) }
try create(index: name, on: table, columns: columns, options: options, condition: condition)
}
/// Creates an index on the specified table and columns.
///
/// For example:
///
/// ```swift
/// // CREATE INDEX index_player_on_email ON player(email)
/// try db.create(index: "index_player_on_email", on: "player", columns: ["email"])
/// ```
///
/// To create a unique index, specify the `.unique` option:
///
/// ```swift
/// // CREATE UNIQUE INDEX index_player_on_email ON player(email)
/// try db.create(index: "index_player_on_email", on: "player", columns: ["email"], options: .unique)
/// ```
///
/// SQLite can also index expressions (<https://www.sqlite.org/expridx.html>)
/// and use specific collations. To create such an index, use a raw SQL
/// query:
///
/// ```swift
/// try db.execute(sql: "CREATE INDEX ...")
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createindex.html>
///
/// - parameters:
/// - name: The index name.
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - options: Index creation options.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
index name: String,
on table: String,
columns: [String],
options: IndexOptions = [],
condition: (any SQLExpressible)? = nil)
throws
{
let index = IndexDefinition(
name: name,
table: table,
expressions: columns.map { .column($0) },
options: options,
condition: condition?.sqlExpression)
let generator = SQLIndexGenerator(index: index)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Creates an index on the specified table and expressions.
///
/// This method can generally create indexes on expressions (see
/// <https://www.sqlite.org/expridx.html>):
///
/// ```swift
/// // CREATE INDEX txy ON t(x+y)
/// try db.create(
/// index: "txy",
/// on: "t",
/// expressions: [Column("x") + Column("y")])
/// ```
///
/// In particular, you can specify the collation on indexed
/// columns (see <https://www.sqlite.org/lang_createindex.html#collations>):
///
/// ```swift
/// // CREATE INDEX index_player_name ON player(name COLLATE NOCASE)
/// try db.create(
/// index: "index_player_name",
/// on: "player",
/// expressions: [Column("name").collating(.nocase)])
/// ```
///
/// - parameters:
/// - name: The index name.
/// - table: The name of the indexed table.
/// - expressions: The indexed expressions.
/// - options: Index creation options.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
index name: String,
on table: String,
expressions: [any SQLExpressible],
options: IndexOptions = [],
condition: (any SQLExpressible)? = nil)
throws
{
let index = IndexDefinition(
name: name,
table: table,
expressions: expressions.map { $0.sqlExpression },
options: options,
condition: condition?.sqlExpression)
let generator = SQLIndexGenerator(index: index)
let sql = try generator.sql(self)
try execute(sql: sql)
}
/// Creates an index with a default name on the specified table and columns.
///
/// The created index is named after the table and the column name(s):
///
/// ```swift
/// // CREATE INDEX index_player_on_email ON player(email)
/// try db.create(indexOn: "player", columns: ["email"])
/// ```
///
/// To create a unique index, specify the `.unique` option:
///
/// ```swift
/// // CREATE UNIQUE INDEX index_player_on_email ON player(email)
/// try db.create(indexOn: "player", columns: ["email"], options: .unique)
/// ```
///
/// In order to specify the index name, use
/// ``create(index:on:columns:options:condition:)`` instead.
///
/// SQLite can also index expressions (<https://www.sqlite.org/expridx.html>)
/// and use specific collations. To create such an index, use
/// ``create(index:on:expressions:options:condition:)``.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createindex.html>
///
/// - parameters:
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - options: Index creation options.
/// - condition: If not nil, creates a partial index
/// (see <https://www.sqlite.org/partialindex.html>).
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(
indexOn table: String,
columns: [String],
options: IndexOptions = [],
condition: (any SQLExpressible)? = nil)
throws
{
try create(
index: Database.defaultIndexName(on: table, columns: columns),
on: table,
columns: columns,
options: options,
condition: condition)
}
/// Deletes a database index.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_dropindex.html>
///
/// - parameter name: The index name.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(index name: String) throws {
try execute(sql: "DROP INDEX \(name.quotedDatabaseIdentifier)")
}
/// Deletes the database index on the specified table and columns
/// if exactly one such index exists.
///
/// - parameters:
/// - table: The name of the indexed table.
/// - columns: The indexed columns.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func drop(indexOn table: String, columns: [String]) throws {
let lowercasedColumns = columns.map { $0.lowercased() }
let indexes = try indexes(on: table).filter { index in
index.columns.map({ $0.lowercased() }) == lowercasedColumns
}
if let index = indexes.first, indexes.count == 1 {
try drop(index: index.name)
}
}
/// Deletes and recreates from scratch all indices that use this collation.
///
/// This method is useful when the definition of a collation sequence
/// has changed.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_reindex.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func reindex(collation: Database.CollationName) throws {
try execute(sql: "REINDEX \(collation.rawValue)")
}
/// Deletes and recreates from scratch all indices that use this collation.
///
/// This method is useful when the definition of a collation sequence
/// has changed.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_reindex.html>
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func reindex(collation: DatabaseCollation) throws {
try reindex(collation: Database.CollationName(rawValue: collation.name))
}
}
/// View creation options
public struct ViewOptions: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// Only creates the view if it does not already exist.
public static let ifNotExists = ViewOptions(rawValue: 1 << 0)
/// Creates a temporary view.
public static let temporary = ViewOptions(rawValue: 1 << 1)
}
@@ -0,0 +1,111 @@
/// Describes an association in the database schema.
///
/// You get instances of `ForeignKeyDefinition` when you create a database
/// tables. For example:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.belongsTo("team") // ForeignKeyDefinition
/// }
/// ```
///
/// See ``TableDefinition/belongsTo(_:inTable:onDelete:onUpdate:deferred:indexed:)``.
public final class ForeignKeyDefinition {
enum Indexing {
case index
case unique
}
var name: String
var table: String?
var deleteAction: Database.ForeignKeyAction?
var updateAction: Database.ForeignKeyAction?
var indexing: Indexing?
var isDeferred: Bool
var notNullConflictResolution: Database.ConflictResolution?
init(
name: String,
table: String?,
deleteAction: Database.ForeignKeyAction?,
updateAction: Database.ForeignKeyAction?,
isIndexed: Bool,
isDeferred: Bool)
{
self.name = name
self.table = table
self.deleteAction = deleteAction
self.updateAction = updateAction
self.indexing = isIndexed ? .index : nil
self.isDeferred = isDeferred
}
/// Adds a not null constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player(
/// // teamId INTEGER NOT NULL REFERENCES team(id)
/// // )
/// try db.create(table: "player") { t in
/// t.belongsTo("team").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 definition of
/// the association.
@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(
/// // teamId INTEGER UNIQUE REFERENCES team(id)
/// // )
/// try db.create(table: "player") { t in
/// t.belongsTo("team").unique()
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#uniqueconst>
///
/// - returns: `self` so that you can further refine the definition of
/// the association.
@discardableResult
public func unique() -> Self {
indexing = .unique
return self
}
func primaryKey(_ db: Database) throws -> SQLPrimaryKeyDescriptor {
if let table {
return try SQLPrimaryKeyDescriptor.find(db, table: table)
}
if try db.tableExists(name) {
return try SQLPrimaryKeyDescriptor.find(db, table: name)
}
let pluralizedName = name.pluralized
if try db.tableExists(pluralizedName) {
return try SQLPrimaryKeyDescriptor.find(db, table: pluralizedName)
}
throw DatabaseError.noSuchTable(name)
}
}
// Explicit non-conformance to Sendable: `ForeignKeyDefinition` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension ForeignKeyDefinition: Sendable { }
@@ -0,0 +1,26 @@
struct IndexDefinition {
let name: String
let table: String
let expressions: [SQLExpression]
let options: IndexOptions
let condition: SQLExpression?
}
/// Index creation options
public struct IndexOptions: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// Only creates the index if it does not already exist.
public static let ifNotExists = IndexOptions(rawValue: 1 << 0)
/// Creates a unique index.
public static let unique = IndexOptions(rawValue: 1 << 1)
}
extension Database {
static func defaultIndexName(on table: String, columns: [String]) -> String {
"index_\(table)_on_\(columns.joined(separator: "_"))"
}
}
@@ -0,0 +1,168 @@
/// A `TableDefinition` lets you modify the components of a database table.
///
/// You don't create instances of this class. Instead, you use the `Database`
/// ``Database/alter(table:body:)`` method:
///
/// ```swift
/// try db.alter(table: "player") { t in // t is TableAlteration
/// t.add(column: "bonus", .integer)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
public final class TableAlteration {
let name: String
enum TableAlterationKind {
case add(ColumnDefinition)
case addColumnLiteral(SQL)
case rename(old: String, new: String)
case drop(String)
}
var alterations: [TableAlterationKind] = []
init(name: String) {
self.name = name
}
/// Appends a column.
///
/// For example:
///
/// ```swift
/// // ALTER TABLE player ADD COLUMN bonus integer
/// try db.alter(table: "player") { t in
/// t.add(column: "bonus", .integer)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameter name: the column name.
/// - parameter type: the column type.
/// - returns: An ColumnDefinition that allows you to refine the
/// column definition.
@discardableResult
public func add(column name: String, _ type: Database.ColumnType? = nil) -> ColumnDefinition {
let column = ColumnDefinition(name: name, type: type)
alterations.append(.add(column))
return column
}
/// Appends a column.
///
/// For example:
///
/// ```swift
/// // ALTER TABLE player ADD COLUMN bonus integer
/// try db.alter(table: "player") { t in
/// t.addColumn(sql: "bonus integer")
/// }
/// ```
public func addColumn(sql: String) {
alterations.append(.addColumnLiteral(SQL(sql: sql)))
}
/// Appends a column.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // ALTER TABLE player ADD COLUMN name TEXT DEFAULT 'Anonymous'
/// try db.alter(table: "player") { t in
/// t.addColumn(literal: "name TEXT DEFAULT \(defaultName)")
/// }
/// ```
public func addColumn(literal: SQL) {
alterations.append(.addColumnLiteral(literal))
}
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// Renames a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.rename(column: "url", to: "homeURL")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameter name: the old name of the column.
/// - parameter newName: the new name of the column.
public func rename(column name: String, to newName: String) {
_rename(column: name, to: newName)
}
/// Drops a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.drop(column: "age")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - Parameter name: the name of the column to drop.
public func drop(column name: String) {
_drop(column: name)
}
#else
/// Renames a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.rename(column: "url", to: "homeURL")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - parameter name: the old name of the column.
/// - parameter newName: the new name of the column.
@available(iOS 13, tvOS 13, watchOS 6, *) // SQLite 3.25+
public func rename(column name: String, to newName: String) {
_rename(column: name, to: newName)
}
/// Drops a column.
///
/// For example:
///
/// ```swift
/// try db.alter(table: "player") { t in
/// t.drop(column: "age")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_altertable.html>
///
/// - Parameter name: the name of the column to drop.
@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) // SQLite 3.35.0+
public func drop(column name: String) {
_drop(column: name)
}
#endif
private func _rename(column name: String, to newName: String) {
alterations.append(.rename(old: name, new: newName))
}
private func _drop(column name: String) {
alterations.append(.drop(name))
}
}
// Explicit non-conformance to Sendable: `TableAlteration` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension TableAlteration: Sendable { }
@@ -0,0 +1,760 @@
/// Table creation options.
public struct TableOptions: OptionSet, Sendable {
public let rawValue: Int
public init(rawValue: Int) { self.rawValue = rawValue }
/// Only creates the table if it does not already exist.
public static let ifNotExists = TableOptions(rawValue: 1 << 0)
/// Creates a temporary table.
public static let temporary = TableOptions(rawValue: 1 << 1)
/// Creates a [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) table.
///
/// Such tables can not be tracked with <doc:DatabaseObservation> tools.
public static let withoutRowID = TableOptions(rawValue: 1 << 2)
#if GRDBCUSTOMSQLITE || GRDBCIPHER
/// Creates a [STRICT](https://www.sqlite.org/stricttables.html) table.
public static let strict = TableOptions(rawValue: 1 << 3)
#else
/// Creates a [STRICT](https://www.sqlite.org/stricttables.html) table.
@available(iOS 15.4, macOS 12.4, tvOS 15.4, watchOS 8.5, *) // SQLite 3.37+
public static let strict = TableOptions(rawValue: 1 << 3)
#endif
}
/// A `TableDefinition` lets you define the components of a database table.
///
/// See the documentation of the `Database`
/// ``Database/create(table:options:body:)`` method for usage information:
///
/// ```swift
/// try db.create(table: "player") { t in // t is TableDefinition
/// t.autoIncrementedPrimaryKey("id")
/// t.column("name", .text).notNull()
/// }
/// ```
///
/// ## Topics
///
/// ### Define Columns
///
/// - ``column(_:_:)``
/// - ``column(literal:)``
/// - ``column(sql:)``
/// - ``ColumnDefinition``
///
/// ### Define the Primary Key
///
/// - ``autoIncrementedPrimaryKey(_:onConflict:)``
/// - ``primaryKey(_:_:onConflict:)``
/// - ``primaryKey(onConflict:body:)``
/// - ``primaryKey(_:onConflict:)``
///
/// ### Define a Foreign Key
///
/// - ``belongsTo(_:inTable:onDelete:onUpdate:deferred:indexed:)``
/// - ``foreignKey(_:references:columns:onDelete:onUpdate:deferred:)``
/// - ``ForeignKeyDefinition``
///
/// ### Define a Unique Key
///
/// - ``uniqueKey(_:onConflict:)``
///
/// ### Define Others Constraints
///
/// - ``check(_:)-6u1za``
/// - ``check(_:)-jpcg``
/// - ``check(sql:)``
/// - ``constraint(literal:)``
/// - ``constraint(sql:)``
public final class TableDefinition {
struct KeyConstraint {
enum Component {
case columnName(String)
case columnDefinition(ColumnDefinition)
case foreignKeyDefinition(ForeignKeyDefinition)
}
var components: [Component]
var conflictResolution: Database.ConflictResolution?
init(components: [Component], conflictResolution: Database.ConflictResolution?) {
self.components = components
self.conflictResolution = conflictResolution
}
init(columns: [String], conflictResolution: Database.ConflictResolution?) {
let components = columns.map { name in
Component.columnName(name)
}
self.init(components: components, conflictResolution: conflictResolution)
}
}
enum ColumnComponent {
case columnDefinition(ColumnDefinition)
case columnLiteral(SQL)
case foreignKeyDefinition(ForeignKeyDefinition)
case foreignKeyConstraint(SQLForeignKeyConstraint)
}
let name: String
let options: TableOptions
var columnComponents: [ColumnComponent] = []
var inPrimaryKeyBody = false
var primaryKeyConstraint: KeyConstraint?
var uniqueKeyConstraints: [KeyConstraint] = []
var checkConstraints: [SQLExpression] = []
var literalConstraints: [SQL] = []
init(name: String, options: TableOptions) {
self.name = name
self.options = options
}
/// Appends an auto-incremented primary key column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
/// ```
///
/// The auto-incremented primary key is an integer primary key that
/// automatically generates unused values when you do not explicitly
/// provide one, and prevents the reuse of ids over the lifetime of
/// the database.
///
/// Related SQLite documentation:
/// - <https://www.sqlite.org/lang_createtable.html#primkeyconst>
/// - <https://www.sqlite.org/lang_createtable.html#rowid>
///
/// - parameter conflictResolution: An optional conflict resolution
/// (see <https://www.sqlite.org/lang_conflict.html>).
/// - returns: `self` so that you can further refine the column definition.
@discardableResult
public func autoIncrementedPrimaryKey(
_ name: String,
onConflict conflictResolution: Database.ConflictResolution? = nil)
-> ColumnDefinition
{
column(name, .integer).primaryKey(onConflict: conflictResolution, autoincrement: true)
}
/// Appends a primary key column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE country (
/// // isoCode TEXT NOT NULL PRIMARY KEY
/// // )
/// try db.create(table: "country") { t in
/// t.primaryKey("isoCode", .text)
/// }
/// ```
///
/// - parameter name: the column name.
/// - parameter type: the column type.
/// - returns: A ``ColumnDefinition`` that allows you to refine the
/// column definition.
@discardableResult
public func primaryKey(
_ name: String,
_ type: Database.ColumnType,
onConflict conflictResolution: Database.ConflictResolution? = nil)
-> ColumnDefinition
{
let pk = column(name, type).primaryKey(onConflict: conflictResolution)
if type == .integer {
// INTEGER PRIMARY KEY is always NOT NULL
return pk
} else {
// Add a not null constraint in order to fix an SQLite bug:
// <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
return pk.notNull()
}
}
/// Defines the primary key on wrapped columns.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE passport (
/// // citizenId INTEGER NOT NULL,
/// // countryCode TEXT NOT NULL,
/// // issueDate DATE NOT NULL,
/// // PRIMARY KEY (citizenId, countryCode)
/// // )
/// try db.create(table: "passport") { t in
/// t.primaryKey {
/// t.column("citizenId", .integer)
/// t.column("countryCode", .text)
/// }
/// t.column("issueDate", .date).notNull()
/// }
/// ```
///
/// A NOT NULL constraint is always added to the wrapped primary key columns.
public func primaryKey(
onConflict conflictResolution: Database.ConflictResolution? = nil,
body: () throws -> Void)
rethrows
{
guard primaryKeyConstraint == nil else {
// Programmer error
fatalError("can't define several primary keys")
}
primaryKeyConstraint = KeyConstraint(components: [], conflictResolution: conflictResolution)
let oldValue = inPrimaryKeyBody
inPrimaryKeyBody = true
defer { inPrimaryKeyBody = oldValue }
try body()
}
/// Appends a table column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // name TEXT
/// // )
/// try db.create(table: "player") { t in
/// t.column("name", .text)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html#tablecoldef>
///
/// - parameter name: the column name.
/// - parameter type: the eventual column type.
/// - returns: A ``ColumnDefinition`` that allows you to refine the
/// column definition.
@discardableResult
public func column(_ name: String, _ type: Database.ColumnType? = nil) -> ColumnDefinition {
let column = ColumnDefinition(name: name, type: type)
columnComponents.append(.columnDefinition(column))
if inPrimaryKeyBody {
// Add a not null constraint in order to fix an SQLite bug:
// <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
column.notNull()
primaryKeyConstraint!.components.append(.columnDefinition(column))
}
return column
}
/// Appends a table column.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // name TEXT
/// // )
/// try db.create(table: "player") { t in
/// t.column(sql: "name TEXT")
/// }
/// ```
public func column(sql: String) {
column(literal: SQL(sql: sql))
}
/// Appends a table column.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // CREATE TABLE player (
/// // name TEXT DEFAULT 'Anonymous'
/// // )
/// let defaultName = "Anonymous"
/// try db.create(table: "player") { t in
/// t.column(literal: "name TEXT DEFAULT \(defaultName)")
/// }
/// ```
public func column(literal: SQL) {
GRDBPrecondition(!inPrimaryKeyBody, "Primary key columns can not be defined with raw SQL")
columnComponents.append(.columnLiteral(literal))
}
/// Adds a primary key constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE citizenship (
/// // citizenId INTEGER NOT NULL,
/// // countryCode TEXT NOT NULL,
/// // PRIMARY KEY (citizenId, countryCode)
/// // )
/// try db.create(table: "citizenship") { t in
/// t.column("citizenId", .integer).notNull()
/// t.column("countryCode", .text).notNull()
/// t.primaryKey(["citizenId", "countryCode"])
/// }
/// ```
///
/// - important: Make sure you add not null constraints on your primary key
/// columns, 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.
///
/// - parameter columns: The primary key columns.
/// - parameter conflictResolution: An optional conflict resolution
/// (see <https://www.sqlite.org/lang_conflict.html>).
public func primaryKey(_ columns: [String], onConflict conflictResolution: Database.ConflictResolution? = nil) {
guard primaryKeyConstraint == nil else {
// Programmer error
fatalError("can't define several primary keys")
}
primaryKeyConstraint = KeyConstraint(columns: columns, conflictResolution: conflictResolution)
}
/// Adds a unique constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE place (
/// // latitude DOUBLE,
/// // longitude DOUBLE,
/// // UNIQUE (latitude, longitude)
/// // )
/// try db.create(table: "place") { t in
/// t.column("latitude", .double)
/// t.column("longitude", .double)
/// t.uniqueKey(["latitude", "longitude"])
/// }
/// ```
///
/// When defining a unique constraint on a single column, you can use the
/// ``ColumnDefinition/unique(onConflict:)`` shortcut:
///
/// ```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 columns: The unique key columns.
/// - parameter conflictResolution: An optional conflict resolution
/// (see <https://www.sqlite.org/lang_conflict.html>).
public func uniqueKey(_ columns: [String], onConflict conflictResolution: Database.ConflictResolution? = nil) {
uniqueKeyConstraints.append(KeyConstraint(columns: columns, conflictResolution: conflictResolution))
}
/// Adds a foreign key.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE passport (
/// // issueDate DATE NOT NULL,
/// // citizenId INTEGER NOT NULL,
/// // countryCode INTEGER NOT NULL,
/// // FOREIGN KEY (citizenId, countryCode)
/// // REFERENCES citizenship(citizenId, countryCode)
/// // ON DELETE CASCADE
/// // )
/// try db.create(table: "passport") { t in
/// t.column("issueDate", .date).notNull()
/// t.column("citizenId", .integer).notNull()
/// t.column("countryCode", .text).notNull()
/// t.foreignKey(["citizenId", "countryCode"], references: "citizenship", onDelete: .cascade)
/// }
/// ```
///
/// When defining a foreign key on a single column, you can use the
/// ``ColumnDefinition/references(_:column:onDelete:onUpdate:deferred:)``
/// shortcut:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("teamId", .integer).references("team", onDelete: .cascade)
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>
///
/// - parameters:
/// - columns: The foreign key columns.
/// - table: The referenced table.
/// - destinationColumns: The columns in the referenced table. If not
/// specified, the columns of the primary key of the referenced table
/// are 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>.
public func foreignKey(
_ columns: [String],
references table: String,
columns destinationColumns: [String]? = nil,
onDelete deleteAction: Database.ForeignKeyAction? = nil,
onUpdate updateAction: Database.ForeignKeyAction? = nil,
deferred isDeferred: Bool = false)
{
let foreignKeyConstraint = SQLForeignKeyConstraint(
columns: columns,
destinationTable: table,
destinationColumns: destinationColumns,
deleteAction: deleteAction,
updateAction: updateAction,
isDeferred: isDeferred)
columnComponents.append(.foreignKeyConstraint(foreignKeyConstraint))
}
/// Declares an association to another table.
///
/// `belongsTo` appends as many columns as there are columns in the
/// primary key of the referenced table, and declares a foreign key that
/// guarantees schema integrity. All primary keys are supported,
/// including composite primary keys that span several columns, and the
/// hidden `rowid` column.
///
/// Added columns are prefixed with `name`, and end with the name of the
/// matching column in the primary key of the referenced table. In the
/// following example, `belongsTo("team")` adds a `teamId` column, and
/// `belongsTo("country")` adds a `countryCode` column:
///
/// ```swift
/// try db.create(table: "team") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
/// try db.create(table: "country") { t in
/// t.primaryKey("code", .text)
/// }
///
/// // CREATE TABLE player (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // teamId INTEGER REFERENCES team(id),
/// // countryCode TEXT NOT NULL REFERENCES country(code),
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("team")
/// t.belongsTo("country").notNull()
/// }
/// ```
///
/// When in doubt, you can check the names of the created columns:
///
/// ```swift
/// // Prints ["id", "teamId", "countryCode"]
/// try print(db.columns(in: "player").map(\.name))
/// ```
///
/// Singular names can refer to database tables whose name is plural:
///
/// ```swift
/// try db.create(table: "teams") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
/// try db.create(table: "countries") { t in
/// t.primaryKey("code", .text)
/// }
///
/// // CREATE TABLE players (
/// // teamId INTEGER REFERENCES teams(id),
/// // countryCode TEXT REFERENCES countries(code),
/// // )
/// try db.create(table: "players") { t in
/// t.belongsTo("team")
/// t.belongsTo("country")
/// }
/// ```
///
/// When the added columns should have a custom prefix, specify an
/// explicit table name:
///
/// ```swift
/// // CREATE TABLE player (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // captainId INTEGER REFERENCES player(id),
/// // )
/// try db.create(table: "player") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("captain", inTable: "player")
/// }
///
/// // CREATE TABLE book (
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
/// // authorId INTEGER REFERENCES person(id),
/// // translatorId INTEGER REFERENCES person(id),
/// // title TEXT
/// // )
/// try db.create(table: "book") { t in
/// t.autoIncrementedPrimaryKey("id")
/// t.belongsTo("author", inTable: "person")
/// t.belongsTo("translator", inTable: "person")
/// t.column("title", .text)
/// }
/// ```
///
/// Specify foreign key actions:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.belongsTo("team", onDelete: .cascade)
/// t.belongsTo("captain", inTable: "player", onDelete: .setNull)
/// }
/// ```
///
/// The added columns are indexed by default. You can disable this
/// automatic index with the `indexed: false` option. You can also make
/// this index unique with ``ForeignKeyDefinition/unique()``:
///
/// ```swift
/// try db.create(table: "player") { t in
/// // teamId is not indexed
/// t.belongsTo("team", indexed: false)
///
/// // One single player per country
/// t.belongsTo("country").unique()
/// }
/// ```
///
/// For more precision in the definition of foreign keys, use instead
/// ``ColumnDefinition/references(_:column:onDelete:onUpdate:deferred:)``
/// or ``TableDefinition/foreignKey(_:references:columns:onDelete:onUpdate:deferred:)``.
/// For example:
///
/// ```swift
/// try db.create(table: "player") { t in
/// // This convenience method...
/// t.belongsTo("team")
///
/// // ... is equivalent to:
/// t.column("teamId", .integer)
/// .references("team")
/// .indexed()
///
/// // ... and is equivalent to:
/// t.column("teamId", .integer).indexed()
/// t.foreignKey(["teamId"], references: "team")
/// }
/// ```
///
/// See [Associations](https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md)
/// for more information about foreign keys and associations.
///
/// - parameters:
/// - name: The name of the foreign key, used as a prefix for the
/// added columns.
/// - table: The referenced table. If nil, the referenced table is
/// designated by the `name` parameter.
/// - 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>.
/// - indexed: A boolean value indicating whether the foreign key is
/// indexed. It is true by default.
/// - returns: A ``ForeignKeyDefinition`` that allows you to refine the
/// foreign key.
@discardableResult
public func belongsTo(
_ name: String,
inTable table: String? = nil,
onDelete deleteAction: Database.ForeignKeyAction? = nil,
onUpdate updateAction: Database.ForeignKeyAction? = nil,
deferred isDeferred: Bool = false,
indexed: Bool = true)
-> ForeignKeyDefinition
{
let foreignKey = ForeignKeyDefinition(
name: name,
table: table,
deleteAction: deleteAction,
updateAction: updateAction,
isIndexed: indexed && !inPrimaryKeyBody,
isDeferred: isDeferred)
columnComponents.append(.foreignKeyDefinition(foreignKey))
if inPrimaryKeyBody {
// Add a not null constraint in order to fix an SQLite bug:
// <https://www.sqlite.org/quirks.html#primary_keys_can_sometimes_contain_nulls>
foreignKey.notNull()
primaryKeyConstraint!.components.append(.foreignKeyDefinition(foreignKey))
}
return foreignKey
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // personalPhone TEXT,
/// // workPhone TEXT,
/// // CHECK personalPhone IS NOT NULL OR workPhone IS NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("personalPhone", .text)
/// t.column("workPhone", .text)
/// let personalPhone = Column("personalPhone")
/// let workPhone = Column("workPhone")
/// t.check(personalPhone != nil || workPhone != nil)
/// }
/// ```
///
/// When defining a check constraint on a single column, you can use the
/// ``ColumnDefinition/check(_:)`` shortcut:
///
/// ```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: The checked condition.
@available(*, deprecated)
public func check(_ condition: some SQLExpressible) {
checkConstraints.append(condition.sqlExpression)
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // personalPhone TEXT,
/// // workPhone TEXT,
/// // CHECK personalPhone IS NOT NULL OR workPhone IS NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("personalPhone", .text)
/// t.column("workPhone", .text)
/// let personalPhone = Column("personalPhone")
/// let workPhone = Column("workPhone")
/// t.check(personalPhone != nil || workPhone != nil)
/// }
/// ```
///
/// When defining a check constraint on a single column, you can use the
/// ``ColumnDefinition/check(_:)`` shortcut:
///
/// ```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: The checked condition.
public func check(_ condition: some SQLSpecificExpressible) {
checkConstraints.append(condition.sqlExpression)
}
/// Adds a check constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // personalPhone TEXT,
/// // workPhone TEXT,
/// // CHECK personalPhone IS NOT NULL OR workPhone IS NOT NULL
/// // )
/// try db.create(table: "player") { t in
/// t.column("personalPhone", .text)
/// t.column("workPhone", .text)
/// t.check(sql: "personalPhone IS NOT NULL OR workPhone IS NOT NULL")
/// }
/// ```
///
/// When defining a check constraint on a single column, you can use the
/// ``ColumnDefinition/check(sql:)`` shortcut:
///
/// ```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
public func check(sql: String) {
checkConstraints.append(SQL(sql: sql).sqlExpression)
}
/// Appends a table constraint.
///
/// For example:
///
/// ```swift
/// // CREATE TABLE player (
/// // score INTEGER,
/// // CHECK (score >= 0)
/// // )
/// try db.create(table: "player") { t in
/// t.column("score", .integer)
/// t.constraint(sql: "CHECK (score >= 0)")
/// }
/// ```
public func constraint(sql: String) {
literalConstraints.append(SQL(sql: sql))
}
/// Appends a table constraint.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// // CREATE TABLE player (
/// // score INTEGER,
/// // CHECK (score >= 0)
/// // )
/// let minScore = 0
/// try db.create(table: "player") { t in
/// t.column("score", .integer)
/// t.constraint(literal: "CHECK (score >= \(minScore))")
/// }
/// ```
public func constraint(literal: SQL) {
literalConstraints.append(literal)
}
}
// Explicit non-conformance to Sendable: `TableDefinition` is a mutable
// class and there is no known reason for making it thread-safe.
@available(*, unavailable)
extension TableDefinition: Sendable { }
@@ -0,0 +1,159 @@
/// The protocol for SQLite virtual table modules.
///
/// The protocol can define a DSL for the
/// ``Database/create(virtualTable:ifNotExists:using:_:)`` `Database` method:
///
/// ```swift
/// let module = ...
/// try db.create(virtualTable: "item", using: module) { t in
/// ...
/// }
/// ```
///
/// GRDB ships with three concrete classes that implement this protocol:
/// ``FTS3``, ``FTS4`` and `FTS5`.
///
/// ## Topics
///
/// ### Configuration Virtual Table Creation
///
/// - ``VirtualTableConfiguration``
public protocol VirtualTableModule {
/// The type of the argument in the
/// ``Database/create(virtualTable:ifNotExists:using:_:)`` closure.
///
/// For example:
///
/// ```swift
/// try db.create(virtualTable: "item", using: module) { t in
/// // t is TableDefinition
/// }
/// ```
associatedtype TableDefinition
/// The name of the module.
var moduleName: String { get }
/// Returns a table definition that is passed as the argument in the
/// ``Database/create(virtualTable:ifNotExists:using:_:)`` closure.
///
/// For example:
///
/// ```swift
/// try db.create(virtualTable: "item", using: module) { t in
/// // t is the result of makeTableDefinition(configuration:)
/// }
/// ```
func makeTableDefinition(configuration: VirtualTableConfiguration) -> TableDefinition
/// Returns the module arguments for the `CREATE VIRTUAL TABLE` query.
func moduleArguments(for definition: TableDefinition, in db: Database) throws -> [String]
/// Execute any relevant database statement after the virtual table has
/// been created.
func database(_ db: Database, didCreate tableName: String, using definition: TableDefinition) throws
}
public struct VirtualTableConfiguration {
/// If true, existing objects must not be replaced, or generate any error
/// (even if they do not match the objects that would be created otherwise.)
var ifNotExists: Bool
}
extension Database {
// MARK: - Database Schema
/// Creates a virtual database table.
///
/// For example:
///
/// ```swift
/// // CREATE VIRTUAL TABLE vocabulary USING spellfix1
/// try db.create(virtualTable: "vocabulary", using: "spellfix1")
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html>
///
/// - parameters:
/// - name: The table name.
/// - ifNotExists: If false (the default), an error is thrown if the
/// table already exists. Otherwise, the table is created unless it
/// already exists.
/// - module: The name of an SQLite virtual table module.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create(virtualTable name: String, ifNotExists: Bool = false, using module: String) throws {
var chunks: [String] = []
chunks.append("CREATE VIRTUAL TABLE")
if ifNotExists {
chunks.append("IF NOT EXISTS")
}
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("USING")
chunks.append(module)
let sql = chunks.joined(separator: " ")
try execute(sql: sql)
}
/// Creates a virtual database table.
///
/// The type of the argument of the `body` function depends on the type of
/// the `module` argument: refer to this module's documentation.
///
/// You can use this method to create full-text virtual tables:
///
/// ```swift
/// try db.create(virtualTable: "book", using: FTS4()) { t in
/// t.column("title")
/// t.column("author")
/// t.column("body")
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_createtable.html>
///
/// - parameters:
/// - name: The table name.
/// - ifNotExists: If false (the default), an error is thrown if the
/// table already exists. Otherwise, the table is created unless it
/// already exists.
/// - module: a virtual module.
/// - body: An optional closure that defines the virtual table.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func create<Module: VirtualTableModule>(
virtualTable tableName: String,
ifNotExists: Bool = false,
using module: Module,
_ body: ((Module.TableDefinition) throws -> Void)? = nil)
throws
{
// Define virtual table
let configuration = VirtualTableConfiguration(ifNotExists: ifNotExists)
let definition = module.makeTableDefinition(configuration: configuration)
if let body {
try body(definition)
}
// Create virtual table
var chunks: [String] = []
chunks.append("CREATE VIRTUAL TABLE")
if ifNotExists {
chunks.append("IF NOT EXISTS")
}
chunks.append(tableName.quotedDatabaseIdentifier)
chunks.append("USING")
let arguments = try module.moduleArguments(for: definition, in: self)
if arguments.isEmpty {
chunks.append(module.moduleName)
} else {
chunks.append(module.moduleName + "(" + arguments.joined(separator: ", ") + ")")
}
let sql = chunks.joined(separator: " ")
try inSavepoint {
try execute(sql: sql)
try module.database(self, didCreate: tableName, using: definition)
return .commit
}
}
}