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: /// - /// - /// /// - 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 /// > 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: /// /// - 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: /// /// - 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: /// /// - 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 , 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: /// /// - 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: /// /// - 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: /// /// - 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 () /// and use specific collations. To create such an index, use /// ``create(index:on:expressions:options:condition:)``. /// /// Related SQLite documentation: /// /// - 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 ). /// - 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 () /// and use specific collations. To create such an index, use a raw SQL /// query: /// /// ```swift /// try db.execute(sql: "CREATE INDEX ...") /// ``` /// /// Related SQLite documentation: /// /// - 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 ). /// - 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 /// ): /// /// ```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 ): /// /// ```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 ). /// - 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 () /// and use specific collations. To create such an index, use /// ``create(index:on:expressions:options:condition:)``. /// /// Related SQLite documentation: /// /// - 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 ). /// - 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: /// /// - 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: /// /// - 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: /// /// - 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) }