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,145 @@
enum SQLColumnGenerator {
case columnDefinition(ColumnDefinition)
case columnLiteral(SQL)
/// - parameter tableName: The name of the table that contains
/// the column.
/// - parameter primaryKeyColumns: A closure that returns the
/// primary key columns in the table that contains the column. If
/// the result is nil, the primary key is the hidden rowID.
func sql(
_ db: Database,
tableName: String,
primaryKeyColumns: () throws -> [SQLColumnDescriptor]?)
throws -> String
{
switch self {
case let .columnDefinition(column):
return try columnSQL(
db, column: column,
tableName: tableName,
primaryKeyColumns: primaryKeyColumns)
case let .columnLiteral(sqlLiteral):
let context = SQLGenerationContext(db, argumentsSink: .literalValues)
return try sqlLiteral.sql(context)
}
}
private func columnSQL(
_ db: Database,
column: ColumnDefinition,
tableName: String,
primaryKeyColumns: () throws -> [SQLColumnDescriptor]?)
throws -> String
{
var chunks: [String] = []
chunks.append(column.name.quotedDatabaseIdentifier)
if let type = column.type {
chunks.append(type.rawValue)
}
if let (conflictResolution, autoincrement) = column.primaryKey {
chunks.append("PRIMARY KEY")
if let conflictResolution {
chunks.append("ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
if autoincrement {
chunks.append("AUTOINCREMENT")
}
}
switch column.notNullConflictResolution {
case .none:
break
case .abort:
chunks.append("NOT NULL")
case let conflictResolution?:
chunks.append("NOT NULL ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
switch column.indexing {
case .none:
break
case .unique(let conflictResolution):
switch conflictResolution {
case .abort:
chunks.append("UNIQUE")
default:
chunks.append("UNIQUE ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
case .index:
break
}
for checkConstraint in column.checkConstraints {
try chunks.append("CHECK (\(checkConstraint.quotedSQL(db)))")
}
if let defaultExpression = column.defaultExpression {
try chunks.append("DEFAULT \(defaultExpression.quotedSQL(db))")
}
if let collationName = column.collationName {
chunks.append("COLLATE")
chunks.append(collationName)
}
for constraint in column.foreignKeyConstraints {
chunks.append("REFERENCES")
if let column = constraint.destinationColumn {
// explicit referenced column names
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)\
(\(column.quotedDatabaseIdentifier))
""")
} else {
// implicit reference to primary key
let pkColumns: [String]
if constraint.destinationTable.lowercased() == tableName.lowercased() {
// autoreference
let primaryKeyColumns = try primaryKeyColumns() ?? [.rowID]
pkColumns = primaryKeyColumns.map(\.name)
} else {
pkColumns = try db.primaryKey(constraint.destinationTable).columns
}
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)\
(\(pkColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))
""")
}
if let deleteAction = constraint.deleteAction {
chunks.append("ON DELETE")
chunks.append(deleteAction.rawValue)
}
if let updateAction = constraint.updateAction {
chunks.append("ON UPDATE")
chunks.append(updateAction.rawValue)
}
if constraint.isDeferred {
chunks.append("DEFERRABLE INITIALLY DEFERRED")
}
}
if let constraint = column.generatedColumnConstraint {
try chunks.append("GENERATED ALWAYS AS (\(constraint.expression.quotedSQL(db)))")
let qualificationLiteral: String
switch constraint.qualification {
case .stored:
qualificationLiteral = "STORED"
case .virtual:
qualificationLiteral = "VIRTUAL"
}
chunks.append(qualificationLiteral)
}
return chunks.joined(separator: " ")
}
}
@@ -0,0 +1,567 @@
/// SQLGenerationContext supports SQL generation:
///
/// - It provides a database connection during SQL generation, for any purpose
/// such as schema introspection.
///
/// - It provides unique table aliases in order to disambiguates table names
/// and columns.
///
/// - It gathers SQL arguments in order to prevent SQL injection.
final class SQLGenerationContext {
private enum Parent {
case none(db: Database, argumentsSink: StatementArgumentsSink)
case context(SQLGenerationContext)
}
/// A database connection.
var db: Database {
switch parent {
case let .none(db: db, argumentsSink: _): return db
case let .context(context): return context.db
}
}
/// All gathered arguments
var arguments: StatementArguments { argumentsSink.arguments }
/// Access to the database connection, the arguments sink, ctes, and resolved
/// names of table aliases from outer contexts (useful in case of
/// subquery generation).
private let parent: Parent
/// The arguments sink which prevents SQL injection.
private var argumentsSink: StatementArgumentsSink {
switch parent {
case let .none(db: _, argumentsSink: argumentsSink): return argumentsSink
case let .context(context): return context.argumentsSink
}
}
private let resolvedNames: [TableAlias: String]
private let ownAliases: Set<TableAlias>
private let ownCTEs: [String: SQLCTE]
/// Creates a generation context.
///
/// - parameter db: A database connection.
/// - parameter argumentsSink: An arguments sink.
/// - parameter aliases: An array of table aliases to disambiguate.
/// - parameter ctes: An dictionary of available CTEs.
init(
_ db: Database,
argumentsSink: StatementArgumentsSink = StatementArgumentsSink(),
aliases: [TableAlias] = [],
ctes: OrderedDictionary<String, SQLCTE> = [:])
{
self.parent = .none(db: db, argumentsSink: argumentsSink)
self.resolvedNames = aliases.resolvedNames
self.ownAliases = Set(aliases)
self.ownCTEs = Dictionary(uniqueKeysWithValues: ctes.lazy.map { ($0.lowercased(), $1) })
}
/// Creates a generation context.
///
/// - parameter parent: A parent context.
/// - parameter aliases: An array of table aliases to disambiguate.
/// - parameter ctes: An dictionary of available CTEs.
private init(
parent: SQLGenerationContext,
aliases: [TableAlias],
ctes: OrderedDictionary<String, SQLCTE>)
{
self.parent = .context(parent)
self.resolvedNames = aliases.resolvedNames
self.ownAliases = Set(aliases)
self.ownCTEs = Dictionary(uniqueKeysWithValues: ctes.lazy.map { ($0.lowercased(), $1) })
}
/// Returns a generation context suitable for subqueries.
func subqueryContext(
aliases: [TableAlias] = [],
ctes: OrderedDictionary<String, SQLCTE> = [:]) -> SQLGenerationContext
{
SQLGenerationContext(parent: self, aliases: aliases, ctes: ctes)
}
/// Returns whether arguments could be appended.
///
/// A false result means that the generation context does not support
/// SQL arguments, and `?` placeholders are not supported.
/// This happens, for example, when we are creating tables:
///
/// // CREATE TABLE player (
/// // name TEXT DEFAULT 'Anonymous' -- String literal instead of ?
/// // )
/// let defaultName = "Anonymous"
/// try db.create(table: "player") { t in
/// t.column(literal: "name TEXT DEFAULT \(defaultName)")
/// }
///
/// A false result is turned into a fatal error when the user uses
/// SQL arguments at unsupported locations:
///
/// // Fatal error:
/// // Not implemented: turning an SQL parameter into an SQL literal value
/// let defaultName = "Anonymous"
/// let literal = SQL(sql: "name TEXT DEFAULT ?", arguments: [defaultName])
/// try db.create(table: "player") { t in
/// t.column(literal: literal)
/// }
func append(arguments: StatementArguments) -> Bool {
argumentsSink.append(arguments: arguments)
}
/// May be nil, when a qualifier is not needed:
///
/// WHERE <qualifier>.column == 1
/// SELECT <qualifier>.*
///
/// WHERE column == 1
/// SELECT *
func qualifier(for alias: TableAlias) -> String? {
if alias.hasUserName {
return alias.identityName
}
if !ownAliases.contains(alias) {
return resolvedName(for: alias)
}
if ownAliases.count > 1 {
return resolvedName(for: alias)
}
return nil
}
/// WHERE <resolvedName> MATCH pattern
func resolvedName(for alias: TableAlias) -> String {
if let name = resolvedNames[alias] {
return name
}
switch parent {
case .none:
return alias.identityName
case let .context(context):
return context.resolvedName(for: alias)
}
}
/// FROM tableName <alias>
func aliasName(for alias: TableAlias) -> String? {
let resolvedName = self.resolvedName(for: alias)
if resolvedName != alias.tableName {
return resolvedName
}
return nil
}
func columnCount(in tableName: String) throws -> Int {
if let cte = ownCTEs[tableName.lowercased()] {
return try cte.columnCount(db)
}
switch parent {
case let .context(context):
return try context.columnCount(in: tableName)
case let .none(db: db, argumentsSink: _):
return try db.columns(in: tableName).count
}
}
}
/// A class that gathers statement arguments, and can be shared between
/// several SQLGenerationContext.
class StatementArgumentsSink {
private(set) var arguments: StatementArguments
private let rawSQL: Bool
/// A sink which turns all argument values into SQL literals.
///
/// The `"WHERE name = \("O'Brien")"` SQL literal is turned into the
/// `WHERE name = 'O''Brien'` SQL.
static let literalValues = StatementArgumentsSink(rawSQL: true)
private init(rawSQL: Bool) {
self.arguments = []
self.rawSQL = rawSQL
}
/// A sink which turns all argument values into `?` SQL parameters.
///
/// The `"WHERE name = \("O'Brien")"` SQL literal is turned into the
/// `WHERE name = ?` SQL.
convenience init() {
self.init(rawSQL: false)
}
// fileprivate so that SQLGenerationContext.append(arguments:) is the only
// available api.
/// Returns false for SQLGenerationContext.rawSQLContext
fileprivate func append(arguments: StatementArguments) -> Bool {
if arguments.isEmpty {
return true
}
if rawSQL {
return false
}
self.arguments += arguments
return true
}
}
// MARK: - TableAlias
/// A TableAlias identifies a table in a request.
///
/// See ``TableRequest/aliased(_:)`` for more information and examples.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
public class TableAlias {
private enum Impl {
/// A TableAlias is undefined when it is created by the GRDB user:
///
/// let alias = TableAlias()
/// let alias = TableAlias(name: "custom")
case undefined(userName: String?)
/// A TableAlias is a table when explicitly specified:
///
/// let alias = TableAlias(tableName: "player")
///
/// Or when it qualifies a request that wasn't qualified yet (in which
/// case it turns from undefined to a table):
///
/// // SELECT custom.* FROM player custom
/// let alias = TableAlias(name: "custom")
/// let request = Player.all().aliased(alias)
case table(tableName: String, userName: String?)
/// A TableAlias can be a proxy for another table alias. Two different
/// instances for the same table identifier:
///
/// // Pointless example: make alias2 a proxy for alias1
/// let alias1 = TableAlias()
/// let alias2 = TableAlias()
/// Player.all()
/// .aliased(alias1)
/// .aliased(alias2)
///
/// Proxies are useful because queries get implicit aliases as soon
/// as they are joined with associations. In the example below,
/// customAlias becomes a proxy for the request's implicit alias, which
/// gets a custom name. This allows implicit and user aliases to merge
/// into a single "table identifier" that matches the user's expectations:
///
/// // SELECT custom.*, team.*
/// // FROM player custom
/// // JOIN team ON taem.id = custom.teamId
/// // WHERE custom.name = 'Arthur'
/// let customAlias = TableAlias(name: "custom")
/// let request = Player
/// .including(required: Player.team)
/// .filter(sql: "custom.name = 'Arthur'")
/// .aliased(customAlias)
case proxy(TableAlias)
}
private var impl: Impl
/// Resolve all proxies
private var root: TableAlias {
if case .proxy(let base) = impl {
return base.root
} else {
return self
}
}
// exposed to SQLGenerationContext
fileprivate var identityName: String {
userName ?? tableName
}
// exposed to SQLGenerationContext
fileprivate var hasUserName: Bool {
userName != nil
}
var tableName: String {
switch impl {
case .undefined:
// Likely a GRDB bug
fatalError("Undefined alias has no table name")
case .table(tableName: let tableName, userName: _):
return tableName
case .proxy(let base):
return base.tableName
}
}
private var userName: String? {
switch impl {
case .undefined(let userName):
return userName
case .table(tableName: _, userName: let userName):
return userName
case .proxy(let base):
return base.userName
}
}
/// Creates a TableAlias.
///
/// When the alias is given a name, this name is guaranteed to be used as
/// the table alias in the SQL query:
///
/// ```swift
/// // SELECT p.* FROM player p
/// let alias = TableAlias(name: "p")
/// let request = Player.all().aliased(alias)
/// ```
public init(name: String? = nil) {
self.impl = .undefined(userName: name)
}
init(tableName: String, userName: String? = nil) {
self.impl = .table(tableName: tableName, userName: userName)
}
func becomeProxy(of base: TableAlias) {
if self === base {
return
}
switch impl {
case let .undefined(userName):
if let userName {
// rename
assert(base.userName == nil || base.userName == userName)
base.setUserName(userName)
}
self.impl = .proxy(base)
case let .table(tableName: tableName, userName: userName):
assert(tableName == base.tableName)
if let userName {
// rename
assert(base.userName == nil || base.userName == userName)
base.setUserName(userName)
}
self.impl = .proxy(base)
case let .proxy(selfBase):
selfBase.becomeProxy(of: base)
}
}
/// Returns nil if aliases can't be merged (conflict in tables, aliases...)
func merged(with other: TableAlias) -> TableAlias? {
if self === other {
return self
}
let root = self.root
let otherRoot = other.root
switch (root.impl, otherRoot.impl) {
case let (.table(tableName: tableName, userName: userName),
.table(tableName: otherTableName, userName: otherUserName)):
guard tableName == otherTableName else {
// can't merge
return nil
}
if let userName, let otherUserName, userName != otherUserName {
// can't merge
return nil
}
root.becomeProxy(of: otherRoot)
return otherRoot
default:
// can't merge
return nil
}
}
private func setUserName(_ userName: String) {
switch impl {
case .undefined:
self.impl = .undefined(userName: userName)
case .table(tableName: let tableName, userName: _):
self.impl = .table(tableName: tableName, userName: userName)
case .proxy(let base):
base.setUserName(userName)
}
}
func setTableName(_ tableName: String) {
switch impl {
case .undefined(let userName):
self.impl = .table(tableName: tableName, userName: userName)
case .table(tableName: let initialTableName, userName: _):
// It is a programmer error to reuse the same TableAlias for
// multiple tables.
//
// // Don't do that
// let alias = TableAlias()
// let books = Book.aliased(alias)...
// let authors = Author.aliased(alias)...
GRDBPrecondition(
tableName.lowercased() == initialTableName.lowercased(),
"A TableAlias most not be used to refer to multiple tables")
case .proxy(let base):
base.setTableName(tableName)
}
}
/// Returns a result column that refers to the aliased table.
public subscript(_ selectable: some SQLSelectable) -> SQLSelection {
// TODO: test
selectable.sqlSelection.qualified(with: self)
}
/// Returns an SQL expression that refers to the aliased table.
///
/// For example, let's sort books by author name first, and then by title:
///
/// ```swift
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.authorId
/// // ORDER BY author.name, book.title
/// let authorAlias = TableAlias()
/// let request = Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias[Column("name")], Column("title"))
/// ```
public subscript(_ expression: some SQLSpecificExpressible & SQLSelectable & SQLOrderingTerm) -> SQLExpression {
expression.sqlExpression.qualified(with: self)
}
public subscript(_ expression: some SQLJSONExpressible &
SQLSpecificExpressible &
SQLSelectable &
SQLOrderingTerm)
-> AnySQLJSONExpressible
{
AnySQLJSONExpressible(sqlExpression: expression.sqlExpression.qualified(with: self))
}
/// Returns an SQL ordering term that refers to the aliased table.
///
/// For example, let's sort books by author name first, and then by title:
///
/// ```swift
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.authorId
/// // ORDER BY author.name ASC, book.title ASC
/// let authorAlias = TableAlias()
/// let request = Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias[Column("name").asc], Column("title").asc)
/// ```
public subscript(_ ordering: some SQLOrderingTerm) -> SQLOrdering {
ordering.sqlOrdering.qualified(with: self)
}
/// Returns an SQL column that refers to the aliased table.
///
/// For example, let's sort books by author name first, and then by title:
///
/// ```swift
/// // SELECT book.*
/// // FROM book
/// // JOIN author ON author.id = book.authorId
/// // ORDER BY author.name, book.title
/// let authorAlias = TableAlias()
/// let request = Book
/// .joining(required: Book.author.aliased(authorAlias))
/// .order(authorAlias["name"], Column("title"))
/// ```
public subscript(_ column: String) -> SQLExpression {
.qualifiedColumn(column, self)
}
/// A boolean SQL expression indicating whether this alias refers to some
/// rows, or not.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// In the example below, we only fetch books that are not associated to
/// any author:
///
/// ```swift
/// struct Author: TableRecord, FetchableRecord { }
/// struct Book: TableRecord, FetchableRecord {
/// static let author = belongsTo(Author.self)
/// }
///
/// try dbQueue.read { db in
/// let authorAlias = TableAlias()
/// let request = Book
/// .joining(optional: Book.author.aliased(authorAlias))
/// .filter(!authorAlias.exists)
/// let books = try request.fetchAll(db)
/// }
/// ```
public var exists: SQLExpression {
SQLExpression.qualifiedExists(self)
}
}
extension TableAlias: Equatable {
public static func == (lhs: TableAlias, rhs: TableAlias) -> Bool {
ObjectIdentifier(lhs.root) == ObjectIdentifier(rhs.root)
}
}
extension TableAlias: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(root))
}
}
extension [TableAlias] {
/// Resolve ambiguities in aliases' names.
fileprivate var resolvedNames: [TableAlias: String] {
// It is a programmer error to reuse the same TableAlias for
// multiple tables.
//
// // Don't do that
// let alias = TableAlias()
// let request = Book
// .including(required: Book.author.aliased(alias)...)
// .including(required: Book.author.aliased(alias)...)
GRDBPrecondition(count == Set(self).count, "A TableAlias most not be used to refer to multiple tables")
let groups = Dictionary(grouping: self) {
$0.identityName.lowercased()
}
var uniqueLowercaseNames: Set<String> = []
var ambiguousGroups: [[TableAlias]] = []
for (lowercaseName, group) in groups {
if group.count > 1 {
// It is a programmer error to reuse the same alias for multiple tables
GRDBPrecondition(
group.countElements(where: \.hasUserName) < 2,
"ambiguous alias: \(group[0].identityName)")
ambiguousGroups.append(group)
} else {
uniqueLowercaseNames.insert(lowercaseName)
}
}
var resolvedNames: [TableAlias: String] = [:]
for group in ambiguousGroups {
var index = 1
for alias in group {
if alias.hasUserName { continue }
let radical = alias.identityName.digitlessRadical
var resolvedName: String
repeat {
resolvedName = "\(radical)\(index)"
index += 1
} while uniqueLowercaseNames.contains(resolvedName.lowercased())
uniqueLowercaseNames.insert(resolvedName.lowercased())
resolvedNames[alias] = resolvedName
}
}
return resolvedNames
}
}
@@ -0,0 +1,42 @@
struct SQLIndexGenerator {
let name: String
let table: String
let expressions: [SQLExpression]
let options: IndexOptions
let condition: SQLExpression?
func sql(_ db: Database) throws -> String {
var sql: SQL = "CREATE"
if options.contains(.unique) {
sql += " UNIQUE"
}
sql += " INDEX"
if options.contains(.ifNotExists) {
sql += " IF NOT EXISTS"
}
sql += " \(identifier: name) ON \(identifier: table)("
sql += expressions.map { SQL($0) }.joined(separator: ", ")
sql += ")"
if let condition {
sql += " WHERE \(condition)"
}
let context = SQLGenerationContext(db, argumentsSink: .literalValues)
return try sql.sql(context)
}
}
extension SQLIndexGenerator {
init(index: IndexDefinition) {
name = index.name
table = index.table
expressions = index.expressions
options = index.options
condition = index.condition
}
}
@@ -0,0 +1,825 @@
/// SQLQueryGenerator is able to generate an SQL SELECT query.
struct SQLQueryGenerator: Refinable {
fileprivate private(set) var relation: SQLQualifiedRelation
private let singleResult: Bool
// For database region
private let prefetchedAssociations: [_SQLAssociation]
/// Creates an SQL query generator.
///
/// - parameter singleResult: A hint as to whether the query should be
/// optimized for a single result.
init(
relation: SQLRelation,
forSingleResult singleResult: Bool = false)
{
self.relation = SQLQualifiedRelation(relation)
self.prefetchedAssociations = relation.prefetchedAssociations
self.singleResult = singleResult
}
func requestSQL(_ context: SQLGenerationContext) throws -> String {
let context = context.subqueryContext(aliases: relation.allAliases, ctes: relation.ctes)
var sql = try commonTableExpressionsPrefix(context)
sql += "SELECT"
if relation.isDistinct {
sql += " DISTINCT"
}
let selection = try relation.selectionPromise.resolve(context.db)
GRDBPrecondition(!selection.isEmpty, "Can't generate SQL with an empty selection")
sql += " "
sql += try selection
.map { try $0.sql(context) }
.joined(separator: ", ")
sql += " FROM "
sql += try relation.source.sql(context)
if relation.joins.isEmpty == false {
let sourceAlias = relation.source.alias
for (_, join) in relation.joins {
sql += " "
sql += try join.sql(context, leftAlias: sourceAlias)
}
}
let filter = try relation.filterPromise?.resolve(context.db)
if let filter {
sql += " WHERE "
sql += try filter.sql(context)
}
let groupExpressions = try relation.groupPromise?.resolve(context.db) ?? []
if !groupExpressions.isEmpty {
sql += " GROUP BY "
sql += try groupExpressions
.map { try $0.sql(context) }
.joined(separator: ", ")
}
if let havingExpression = try relation.havingExpressionPromise?.resolve(context.db) {
sql += " HAVING "
sql += try havingExpression.sql(context)
}
let orderings = try relation.ordering.resolve(context.db)
if !orderings.isEmpty {
sql += " ORDER BY "
sql += try orderings
.map { try $0.sql(context) }
.joined(separator: ", ")
}
var limit = relation.limit
if try limit == nil && singleResult && !expectsSingleResult(
context.db,
selection: selection,
filter: filter,
groupExpressions: groupExpressions)
{
limit = SQLLimit(limit: 1, offset: limit?.offset)
}
if let limit {
sql += " LIMIT "
sql += limit.sql
}
return sql
}
func makePreparedRequest(_ db: Database) throws -> PreparedRequest {
try PreparedRequest(
statement: makeStatement(db),
adapter: rowAdapter(SQLGenerationContext(db, ctes: relation.ctes)))
}
/// The number of fetched columns.
func columnCount(_ db: Database) throws -> Int {
try relation
.selectionPromise
.resolve(db)
.columnCount(SQLGenerationContext(db, ctes: relation.ctes))
}
/// Returns a prepared statement
func makeStatement(_ db: Database) throws -> Statement {
// Build
let context = SQLGenerationContext(db)
let sql = try requestSQL(context)
// Compile & set arguments
let statement = try db.makeStatement(sql: sql)
statement.arguments = context.arguments
// Optimize statement region. This allows us to track individual rowids,
// and also find some provably empty requests such as `Player.none()`.
statement.databaseRegion = try optimizedSelectedRegion(db, statement.databaseRegion)
if !statement.databaseRegion.isEmpty {
// Unless the statement region is provably empty, also append the
// region of prefetched associations.
//
// This makes sure we observe the correct database region when the
// statement is executed, even if we don't actually fetch prefetched
// associations, due to lacking database content.
//
// For example, fetching `parent.including(all: children)` will
// select parents, but won't attempt to select any children if there
// is no parent in the database. And yet we need to observe the
// table for children. This is why we include the prefetched region.
//
// Note that the request `parent.none().including(all: children)` is
// different. Since `parent.none()` is provably empty, its region
// is empty, and we thus avoid this code branch.
let region = try prefetchedRegion(
db,
associations: prefetchedAssociations,
from: relation.source.tableName)
statement.databaseRegion.formUnion(region)
}
return statement
}
/// Returns an optimized database region, when possible.
///
/// The optimized region allows us to track individual rowids, and also
/// discard some provably empty requests such as `Player.none()`.
private func optimizedSelectedRegion(_ db: Database, _ selectedRegion: DatabaseRegion) throws -> DatabaseRegion {
var optimizedRegion = selectedRegion
// Give up unless request feeds from a database table
let tableName = relation.source.tableName
guard try db.tableExists(tableName) else { // skip views
return optimizedRegion
}
// If the request is filtered on rowIds, we can optimize the region:
//
// - Player.filter(Column("id") == 1) // region "player(*)[1]"
// - Player.filter(ids: [1, 2, 3]) // region "player(*)[1, 2, 3]"
// - Player.none() // region "empty"
if let filter = try relation.filterPromise?.resolve(db),
let rowIDs = try filter.identifyingRowIDs(db, for: relation.source.alias)
{
optimizedRegion = optimizedRegion.tableIntersection(tableName, rowIds: rowIDs)
}
return optimizedRegion
}
/// If true, executing this query yields at most one row.
/// If false, we don't know how many rows this query returns.
private func expectsSingleResult(
_ db: Database,
selection: [SQLSelection],
filter: SQLExpression?,
groupExpressions: [SQLExpression])
throws -> Bool
{
if relation.joins.isEmpty == false {
// Don't expect single results as soon as there is a join
return false
}
// Do we filter on a unique key?
let tableName = relation.source.tableName
if try db.tableExists(tableName), // skip views
let identifyingColums = try filter?.identifyingColums(db, for: relation.source.alias),
try db.table(tableName, hasUniqueKey: identifyingColums)
{
// Filter by unique key: guaranteed single row!
return true
}
// Do we aggregate without grouping?
if groupExpressions.isEmpty && selection.contains(where: \.isAggregate) {
// Selection contains an aggregate function: guaranteed single row!Ë
return true
}
return false
}
/// Returns a `DELETE` statement, with `RETURNING` clause if `selection`
/// is not empty.
func makeDeleteStatement(_ db: Database, selection: [any SQLSelectable] = []) throws -> Statement {
switch try grouping(db) {
case .none:
guard relation.joins.isEmpty else {
return try makeTrivialDeleteStatement(db, selection: selection)
}
let context = SQLGenerationContext(db, aliases: relation.allAliases, ctes: relation.ctes)
var sql = try commonTableExpressionsPrefix(context)
sql += try "DELETE FROM " + relation.source.sql(context)
if let filter = try relation.filterPromise?.resolve(db) {
sql += " WHERE "
sql += try filter.sql(context)
}
if let limit = relation.limit {
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY "
sql += try orderings
.map { try $0.sql(context) }
.joined(separator: ", ")
}
sql += " LIMIT " + limit.sql
}
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
case .unique:
return try makeTrivialDeleteStatement(db, selection: selection)
case .nonUnique:
// Programmer error
fatalError("Can't delete query with GROUP BY clause")
}
}
/// DELETE FROM table WHERE id IN (SELECT id FROM table ...)
/// DELETE FROM table WHERE id IN (SELECT id FROM table ...) RETURNING ...
private func makeTrivialDeleteStatement(_ db: Database, selection: [any SQLSelectable]) throws -> Statement {
let tableName = relation.source.tableName
let alias = TableAlias(tableName: tableName)
let context = SQLGenerationContext(db, aliases: [alias])
let subqueryContext = context.subqueryContext(aliases: relation.allAliases, ctes: relation.ctes)
let primaryKey = SQLExpression.fastPrimaryKey
let selectPrimaryKey = self.with {
$0.relation = $0.relation.selectOnly([.expression(primaryKey)])
}
var sql = "DELETE FROM \(tableName.quotedDatabaseIdentifier) WHERE "
sql += try alias[primaryKey].sql(context)
sql += " IN ("
sql += try selectPrimaryKey.requestSQL(subqueryContext)
sql += ")"
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
}
/// Returns an `UPDATE` statement, with `RETURNING` clause if `selection`
/// is not empty.
///
/// Returns nil if assignments is empty.
func makeUpdateStatement(
_ db: Database,
conflictResolution: Database.ConflictResolution,
assignments: [ColumnAssignment],
selection: [any SQLSelectable] = [])
throws -> Statement?
{
switch try grouping(db) {
case .none:
guard relation.joins.isEmpty else {
return try makeTrivialUpdateStatement(
db,
conflictResolution: conflictResolution,
assignments: assignments,
selection: selection)
}
let context = SQLGenerationContext(db, aliases: relation.allAliases, ctes: relation.ctes)
var sql = try commonTableExpressionsPrefix(context)
sql += "UPDATE "
if conflictResolution != .abort {
sql += "OR \(conflictResolution.rawValue) "
}
sql += try relation.source.sql(context)
let updateSQL = try assignments
.compactMap { try $0.sql(context) }
.joined(separator: ", ")
if updateSQL.isEmpty {
return nil
}
sql += " SET \(updateSQL)"
if let filter = try relation.filterPromise?.resolve(db) {
sql += " WHERE "
sql += try filter.sql(context)
}
if let limit = relation.limit {
let orderings = try relation.ordering.resolve(db)
if !orderings.isEmpty {
sql += " ORDER BY "
sql += try orderings
.map { try $0.sql(context) }
.joined(separator: ", ")
}
sql += " LIMIT " + limit.sql
}
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
case .unique:
return try makeTrivialUpdateStatement(
db,
conflictResolution: conflictResolution,
assignments: assignments,
selection: selection)
case .nonUnique:
// Programmer error
fatalError("Can't update query with GROUP BY clause")
}
}
/// UPDATE table SET ... WHERE id IN (SELECT id FROM table ...)
/// UPDATE table SET ... WHERE id IN (SELECT id FROM table ...) RETURNING ...
/// Returns nil if assignments is empty
private func makeTrivialUpdateStatement(
_ db: Database,
conflictResolution: Database.ConflictResolution,
assignments: [ColumnAssignment],
selection: [any SQLSelectable])
throws -> Statement?
{
let tableName = relation.source.tableName
let alias = TableAlias(tableName: tableName)
let context = SQLGenerationContext(db, aliases: [alias])
let subqueryContext = context.subqueryContext(aliases: relation.allAliases, ctes: relation.ctes)
let primaryKey = SQLExpression.fastPrimaryKey
let selectPrimaryKey = self.with {
$0.relation = $0.relation.selectOnly([.expression(primaryKey)])
}
// UPDATE table...
var sql = "UPDATE "
if conflictResolution != .abort {
sql += "OR \(conflictResolution.rawValue) "
}
sql += tableName.quotedDatabaseIdentifier
// SET column = value...
let updateSQL = try assignments
.compactMap { try $0.sql(context) }
.joined(separator: ", ")
if updateSQL.isEmpty {
return nil
}
sql += " SET \(updateSQL)"
// WHERE id IN (SELECT id FROM ...)
sql += " WHERE "
sql += try alias[primaryKey].sql(context)
sql += " IN ("
sql += try selectPrimaryKey.requestSQL(subqueryContext)
sql += ")"
return try makeStatement(db, sql: sql, arguments: context.arguments, returning: selection)
}
// Support for the RETURNING clause
private func makeStatement(
_ db: Database,
sql: String,
arguments: StatementArguments,
returning selection: [any SQLSelectable])
throws -> Statement
{
if selection.isEmpty {
let statement = try db.makeStatement(sql: sql)
statement.arguments = arguments
return statement
} else {
let context = SQLGenerationContext(db)
var sql = sql
var arguments = arguments
sql += " RETURNING "
sql += try selection
.map { try $0.sqlSelection.sql(context) }
.joined(separator: ", ")
arguments += context.arguments
let statement = try db.makeStatement(sql: sql)
statement.arguments = arguments
return statement
}
}
private func commonTableExpressionsPrefix(_ context: SQLGenerationContext) throws -> String {
if relation.ctes.isEmpty {
return ""
}
var sql = "WITH "
if relation.ctes.values.contains(where: \.isRecursive) {
sql += "RECURSIVE "
}
sql += try relation.ctes
.map { tableName, cte in
var columnsSQL = ""
if let columns = cte.columns, !columns.isEmpty {
columnsSQL = "("
columnsSQL += columns
.map(\.quotedDatabaseIdentifier)
.joined(separator: ", ")
columnsSQL += ")"
}
let cteContext = context.subqueryContext()
let subquerySQL = try cte.sqlSubquery.sql(cteContext)
return "\(tableName.quotedDatabaseIdentifier)\(columnsSQL) AS (\(subquerySQL))"
}
.joined(separator: ", ")
sql += " "
return sql
}
/// Informs about the query grouping
private enum GroupingInfo {
/// No grouping at all: SELECT ... FROM player
case none
/// Grouped by unique key: SELECT ... FROM player GROUP BY id
case unique
/// Grouped by some non-unique columnns: SELECT ... FROM player GROUP BY teamId
case nonUnique
}
/// Informs about the query grouping
private func grouping(_ db: Database) throws -> GroupingInfo {
// Empty group clause: no grouping
// SELECT * FROM player
guard let groupExpressions = try relation.groupPromise?.resolve(db), groupExpressions.isEmpty == false else {
return .none
}
// Grouping something which is not a table: assume non unique grouping.
let tableName = relation.source.tableName
guard try db.tableExists(tableName) else { // skip views
return .nonUnique
}
var groupingColumns: Set<String> = []
for expression in groupExpressions {
guard let column = try expression.column(db, for: relation.source.alias, acceptsBijection: true) else {
// Grouping by something which is not a column: assume non
// unique grouping.
return .nonUnique
}
groupingColumns.insert(column)
}
// Grouping by some column(s) which are unique
// SELECT * FROM player GROUP BY id
if try db.table(tableName, hasUniqueKey: groupingColumns) {
return .unique
}
// Grouping by some column(s) which are not unique
// SELECT * FROM player GROUP BY score
return .nonUnique
}
/// Returns the row adapter which presents the fetched rows according to the
/// tree of joined relations.
///
/// The adapter is nil for queries without any included relation,
/// because the fetched rows don't need any processing:
///
/// // SELECT * FROM book
/// let request = Book.all()
/// for row in try Row.fetchAll(db, request) {
/// row // [id:1, title:"Moby-Dick"]
/// let book = try Book(row: row)
/// }
///
/// But as soon as the selection includes columns of a included relation,
/// we need an adapter:
///
/// // SELECT book.*, author.* FROM book JOIN author ON author.id = book.authorId
/// let request = Book.including(required: Book.author)
/// for row in try Row.fetchAll(db, request) {
/// row // [id:1, title:"Moby-Dick"]
/// let book = try Book(row: row)
///
/// row.scopes["author"] // [id:12, name:"Herman Melville"]
/// let author: Author = row["author"]
/// }
private func rowAdapter(_ context: SQLGenerationContext) throws -> (any RowAdapter)? {
try relation.rowAdapter(context, fromIndex: 0, rootRelation: true)?.adapter
}
}
/// To generate SQL, we need a "qualified" relation, where all tables,
/// expressions, etc, are qualified with table aliases.
///
/// All those aliases let us disambiguate tables at the SQL level, and
/// prefix columns names. For example, the following request...
///
/// Book.filter(Column("kind") == Book.Kind.novel)
/// .including(optional: Book.author)
/// .including(optional: Book.translator)
/// .annotated(with: Book.awards.count)
///
/// ... generates the following SQL, where all identifiers are correctly
/// disambiguated and qualified:
///
/// SELECT book.*, person1.*, person2.*, COUNT(DISTINCT award.id)
/// FROM book
/// LEFT JOIN person person1 ON person1.id = book.authorId
/// LEFT JOIN person person2 ON person2.id = book.translatorId
/// LEFT JOIN award ON award.bookId = book.id
/// GROUP BY book.id
/// WHERE book.kind = 'novel'
///
/// `SQLQualifiedRelation` contains the following information:
///
/// WITH ... -- ctes
/// SELECT ... -- selectionPromise
/// FROM ... -- source
/// JOIN ... -- joins
/// WHERE ... -- filterPromise
/// GROUP BY ... -- groupPromise
/// HAVING ... -- havingExpressionPromise
/// ORDER BY ... -- ordering
/// LIMIT ... -- limit
private struct SQLQualifiedRelation {
/// All aliases, including aliases of joined relations
var allAliases: [TableAlias] {
joins.reduce(into: [source.alias].compactMap { $0 }) {
$0.append(contentsOf: $1.value.relation.allAliases)
}
}
/// The source
let source: SQLQualifiedSource
/// The selection from source, not including selection of joined relations
private var sourceSelectionPromise: DatabasePromise<[SQLSelection]>
var isDistinct: Bool
/// The full selection, including selection of joined relations
var selectionPromise: DatabasePromise<[SQLSelection]> {
DatabasePromise { db in
let selection = try sourceSelectionPromise.resolve(db)
return try joins.values.reduce(into: selection) { selection, join in
let joinedSelection = try join.relation.selectionPromise.resolve(db)
selection.append(contentsOf: joinedSelection)
}
}
}
let filterPromise: DatabasePromise<SQLExpression>?
/// The ordering of source, not including ordering of joined relations
private let sourceOrdering: SQLRelation.Ordering
/// The full ordering, including orderings of joined relations
var ordering: SQLRelation.Ordering {
joins.reduce(sourceOrdering) {
$0.appending($1.value.relation.ordering)
}
}
private(set) var joins: OrderedDictionary<String, SQLQualifiedJoin>
let groupPromise: DatabasePromise<[SQLExpression]>?
let havingExpressionPromise: DatabasePromise<SQLExpression>?
let limit: SQLLimit?
let ctes: OrderedDictionary<String, SQLCTE>
init(_ relation: SQLRelation) {
// Qualify the source, so that it be disambiguated with an SQL alias
// if needed (when a select query uses the same table several times).
// This disambiguation job will be actually performed by
// SQLGenerationContext, when the SQLSelectQueryGenerator which owns
// this SQLQualifiedRelation generates SQL.
source = SQLQualifiedSource(relation.source)
// Qualify all selection, filter, etc, so that all identifiers
// can be correctly disambiguated and qualified.
let sourceAlias = source.alias
sourceSelectionPromise = relation.selectionPromise.map {
$0.map { $0.qualified(with: sourceAlias) }
}
filterPromise = relation.filterPromise.map {
$0.map { $0.qualified(with: sourceAlias) }
}
sourceOrdering = relation.ordering.qualified(with: sourceAlias)
groupPromise = relation.groupPromise?.map {
$0.map { $0.qualified(with: sourceAlias) }
}
havingExpressionPromise = relation.havingExpressionPromise.map {
$0.map { $0.qualified(with: sourceAlias) }
}
// Turns relation children into joins. `including(all:)` children are
// discarded on the way.
joins = relation.children.compactMapValues { SQLQualifiedJoin($0) }
// Copy other flags
limit = relation.limit
isDistinct = relation.isDistinct
ctes = relation.allCTEs
}
/// See SQLQueryGenerator.rowAdapter(_:)
///
/// - parameter startIndex: The index of the leftmost selected column of
/// this relation in a full SQL query.
/// - parameter rootRelation: True iff the relation is at the root of a
/// SQLQueryGenerator (as opposed to the joined relations).
/// - returns: An optional tuple made of a RowAdapter and the index past the
/// rightmost selected column of this relation. Nil is returned if this
/// relations does not need any row adapter.
func rowAdapter(
_ context: SQLGenerationContext,
fromIndex startIndex: Int,
rootRelation: Bool) throws
-> (adapter: any RowAdapter, endIndex: Int)?
{
// Root relation && no join => no need for any adapter
if rootRelation && joins.isEmpty {
return nil
}
// The number of columns in source selection. Columns selected by joined
// relations are appended after.
let sourceSelectionWidth = try sourceSelectionPromise.resolve(context.db).columnCount(context)
// Recursively build adapters for each joined relation with a selection.
// Name them according to the join keys.
var endIndex = startIndex + sourceSelectionWidth
var scopes: [String: any RowAdapter] = [:]
for (key, join) in joins {
if let (joinAdapter, joinEndIndex) = try join
.relation
.rowAdapter(context, fromIndex: endIndex, rootRelation: false)
{
scopes[key] = joinAdapter
endIndex = joinEndIndex
}
}
// (Root relation || empty selection) && no included relation => no need for any adapter
if (rootRelation || sourceSelectionWidth == 0) && scopes.isEmpty {
return nil
}
// Build a RangeRowAdapter extended with the adapters of joined relations.
//
// // SELECT book.*, author.* FROM book JOIN author ON author.id = book.authorId
// let request = Book.including(required: Book.author)
// for row in try Row.fetchAll(db, request) {
//
// The RangeRowAdapter hides the columns appended by joined relations:
//
// row // [id:1, title:"Moby-Dick"]
// let book = try Book(row: row)
//
// Scopes give access to those joined relations:
//
// row.scopes["author"] // [id:12, name:"Herman Melville"]
// let author: Author = row["author"]
// }
let rangeAdapter = RangeRowAdapter(startIndex ..< (startIndex + sourceSelectionWidth))
let adapter = rangeAdapter.addingScopes(scopes)
return (adapter: adapter, endIndex: endIndex)
}
/// Sets the selection, removes all selections from joins, and clears the
/// `isDistinct` flag.
func selectOnly(_ selection: [SQLSelection]) -> Self {
let qualifiedSelection = selection.map {
$0.qualified(with: source.alias)
}
return with {
$0.sourceSelectionPromise = DatabasePromise(value: qualifiedSelection)
$0.isDistinct = false
$0.joins = $0.joins.mapValues { join in
join.with {
$0.relation = $0.relation.selectOnly([])
}
}
}
}
}
extension SQLQualifiedRelation: Refinable { }
/// A "qualified" source, where all tables are identified with a table alias.
private struct SQLQualifiedSource {
var tableName: String
var alias: TableAlias
init(_ source: SQLSource) {
self.tableName = source.tableName
self.alias = source.alias ?? TableAlias(tableName: source.tableName)
assert(alias.tableName == tableName)
}
func sql(_ context: SQLGenerationContext) throws -> String {
if let aliasName = context.aliasName(for: alias) {
return "\(tableName.quotedDatabaseIdentifier) \(aliasName.quotedDatabaseIdentifier)"
} else {
return "\(tableName.quotedDatabaseIdentifier)"
}
}
}
/// A "qualified" join, where all tables are identified with a table alias.
private struct SQLQualifiedJoin: Refinable {
enum Kind: String {
case leftJoin = "LEFT JOIN"
case innerJoin = "JOIN"
init?(_ kind: SQLRelation.Child.Kind) {
switch kind {
case .oneRequired:
self = .innerJoin
case .oneOptional:
self = .leftJoin
case .all, .bridge:
// Eager loading of to-many associations is not implemented with joins
return nil
}
}
}
var kind: Kind
var condition: SQLAssociationCondition
var relation: SQLQualifiedRelation
init?(_ child: SQLRelation.Child) {
guard let kind = Kind(child.kind) else {
return nil
}
self.kind = kind
self.condition = child.condition
self.relation = SQLQualifiedRelation(child.relation)
}
func sql(_ context: SQLGenerationContext, leftAlias: TableAlias) throws -> String {
try sql(context, leftAlias: leftAlias, allowingInnerJoin: true)
}
private func sql(
_ context: SQLGenerationContext,
leftAlias: TableAlias,
allowingInnerJoin allowsInnerJoin: Bool)
throws -> String
{
var allowsInnerJoin = allowsInnerJoin
switch self.kind {
case .innerJoin:
guard allowsInnerJoin else {
// TODO: chainOptionalRequired
//
// When we eventually implement this, make sure we both support:
// - joining(optional: assoc.joining(required: ...))
// - having(assoc.joining(required: ...).isEmpty)
fatalError("Not implemented: chaining a required association behind an optional association")
}
case .leftJoin:
allowsInnerJoin = false
}
// JOIN table...
var sql = try "\(kind.rawValue) \(relation.source.sql(context))"
// ... ON <join conditions> AND <other filters>
let rightAlias = relation.source.alias
var conditions: [SQLExpression] = []
if let expression = try condition.joinExpression(context.db,
leftAlias: leftAlias,
rightAlias: rightAlias)
{
conditions.append(expression)
}
if let filter = try relation.filterPromise?.resolve(context.db) {
conditions.append(filter.qualified(with: rightAlias))
}
if conditions.isEmpty == false {
sql += " ON "
sql += try conditions
.joined(operator: .and)
.sql(context)
}
for (_, join) in relation.joins {
// Right becomes left as we dig further
sql += try " \(join.sql(context, leftAlias: rightAlias, allowingInnerJoin: allowsInnerJoin))"
}
return sql
}
}
@@ -0,0 +1,84 @@
struct SQLTableAlterationGenerator {
private enum TableAlterationKind {
case addColumn(SQLColumnGenerator)
case addIndex(SQLIndexGenerator)
case renameColumn(old: String, new: String)
case dropColumn(String)
}
private var name: String
private var alterations: [TableAlterationKind] = []
func sql(_ db: Database) throws -> String {
var statements: [String] = []
for alteration in alterations {
switch alteration {
case let .addColumn(column):
var chunks: [String] = []
chunks.append("ALTER TABLE")
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("ADD COLUMN")
let sql = try column.sql(db, tableName: name, primaryKeyColumns: {
try db.primaryKey(name).columnInfos.map { columnInfos in
columnInfos.map { SQLColumnDescriptor($0) }
}
})
chunks.append(sql)
let statement = chunks.joined(separator: " ")
statements.append(statement)
case let .addIndex(index):
try statements.append(index.sql(db))
case let .renameColumn(oldName, newName):
var chunks: [String] = []
chunks.append("ALTER TABLE")
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("RENAME COLUMN")
chunks.append(oldName.quotedDatabaseIdentifier)
chunks.append("TO")
chunks.append(newName.quotedDatabaseIdentifier)
let statement = chunks.joined(separator: " ")
statements.append(statement)
case let .dropColumn(column):
var chunks: [String] = []
chunks.append("ALTER TABLE")
chunks.append(name.quotedDatabaseIdentifier)
chunks.append("DROP COLUMN")
chunks.append(column.quotedDatabaseIdentifier)
let statement = chunks.joined(separator: " ")
statements.append(statement)
}
}
return statements.joined(separator: "; ")
}
}
extension SQLTableAlterationGenerator {
init(_ tableAlteration: TableAlteration) {
self.name = tableAlteration.name
self.alterations = []
for alteration in tableAlteration.alterations {
switch alteration {
case let .add(column):
alterations.append(.addColumn(.columnDefinition(column)))
if let indexDefinition = column.indexDefinition(in: name) {
alterations.append(.addIndex(SQLIndexGenerator(index: indexDefinition)))
}
case let .addColumnLiteral(sql):
alterations.append(.addColumn(.columnLiteral(sql)))
case let .rename(old: oldName, new: newName):
alterations.append(.renameColumn(old: oldName, new: newName))
case let .drop(column):
alterations.append(.dropColumn(column))
}
}
}
}
@@ -0,0 +1,476 @@
struct SQLTableGenerator {
var name: String
var options: TableOptions
var columnGenerators: [SQLColumnGenerator]
/// Used for auto-referencing foreign keys: we need to know the columns
/// of the primary key before they exist in the database schema, hence
/// the name of "forward" primary key columns.
///
/// If nil, the primary key is the hidden rowID.
var forwardPrimaryKeyColumns: [SQLColumnDescriptor]?
var primaryKeyConstraint: KeyConstraint?
var uniqueKeyConstraints: [KeyConstraint]
var foreignKeyConstraints: [SQLForeignKeyConstraint]
var checkConstraints: [SQLExpression]
var literalConstraints: [SQL]
var indexGenerators: [SQLIndexGenerator]
struct KeyConstraint {
var columns: [String]
var conflictResolution: Database.ConflictResolution?
}
func sql(_ db: Database) throws -> String {
var statements: [String] = []
do {
var chunks: [String] = []
chunks.append("CREATE")
if options.contains(.temporary) {
chunks.append("TEMPORARY")
}
chunks.append("TABLE")
if options.contains(.ifNotExists) {
chunks.append("IF NOT EXISTS")
}
chunks.append(name.quotedDatabaseIdentifier)
do {
var items: [String] = []
try items.append(contentsOf: columnGenerators.map {
try $0.sql(db, tableName: name, primaryKeyColumns: { forwardPrimaryKeyColumns })
})
if let constraint = primaryKeyConstraint {
var chunks: [String] = []
chunks.append("PRIMARY KEY")
chunks.append("(\(constraint.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))")
if let conflictResolution = constraint.conflictResolution {
chunks.append("ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
items.append(chunks.joined(separator: " "))
}
for constraint in uniqueKeyConstraints {
var chunks: [String] = []
chunks.append("UNIQUE")
chunks.append("(\(constraint.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))")
if let conflictResolution = constraint.conflictResolution {
chunks.append("ON CONFLICT")
chunks.append(conflictResolution.rawValue)
}
items.append(chunks.joined(separator: " "))
}
for constraint in foreignKeyConstraints {
var chunks: [String] = []
chunks.append("FOREIGN KEY")
chunks.append("(\(constraint.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", ")))")
chunks.append("REFERENCES")
if let destinationColumns = constraint.destinationColumns {
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)(\
\(destinationColumns.map(\.quotedDatabaseIdentifier).joined(separator: ", "))\
)
""")
} else if constraint.destinationTable.lowercased() == name.lowercased() {
// autoreference
let forwardPrimaryKeyColumns = forwardPrimaryKeyColumns ?? [.rowID]
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)(\
\(forwardPrimaryKeyColumns.map(\.name.quotedDatabaseIdentifier).joined(separator: ", "))\
)
""")
} else {
let primaryKey = try db.primaryKey(constraint.destinationTable)
chunks.append("""
\(constraint.destinationTable.quotedDatabaseIdentifier)(\
\(primaryKey.columns.map(\.quotedDatabaseIdentifier).joined(separator: ", "))\
)
""")
}
if let deleteAction = constraint.deleteAction {
chunks.append("ON DELETE")
chunks.append(deleteAction.rawValue)
}
if let updateAction = constraint.updateAction {
chunks.append("ON UPDATE")
chunks.append(updateAction.rawValue)
}
if constraint.isDeferred {
chunks.append("DEFERRABLE INITIALLY DEFERRED")
}
items.append(chunks.joined(separator: " "))
}
for checkExpression in checkConstraints {
var chunks: [String] = []
try chunks.append("CHECK (\(checkExpression.quotedSQL(db)))")
items.append(chunks.joined(separator: " "))
}
for literal in literalConstraints {
let context = SQLGenerationContext(db, argumentsSink: .literalValues)
try items.append(literal.sql(context))
}
chunks.append("(\(items.joined(separator: ", ")))")
}
var tableOptions: [String] = []
#if GRDBCUSTOMSQLITE || GRDBCIPHER
if options.contains(.strict) {
tableOptions.append("STRICT")
}
#else
if #available(iOS 15.4, macOS 12.4, tvOS 15.4, watchOS 8.5, *) { // SQLite 3.37+
if options.contains(.strict) {
tableOptions.append("STRICT")
}
}
#endif
if options.contains(.withoutRowID) {
tableOptions.append("WITHOUT ROWID")
}
if !tableOptions.isEmpty {
chunks.append(tableOptions.joined(separator: ", "))
}
statements.append(chunks.joined(separator: " "))
}
let indexStatements = try indexGenerators.map { try $0.sql(db) }
statements.append(contentsOf: indexStatements)
return statements.joined(separator: "; ")
}
private struct ForeignKeyGenerator {
var columnNames: [String]
var columnGenerators: [SQLColumnGenerator]
var foreignKeyConstraint: SQLForeignKeyConstraint?
var indexGenerator: SQLIndexGenerator?
}
}
extension SQLTableGenerator {
init(_ db: Database, table: TableDefinition) throws {
var indexOptions: IndexOptions = []
if table.options.contains(.ifNotExists) { indexOptions.insert(.ifNotExists) }
func makeKeyConstraint(
_ db: Database,
constraint: TableDefinition.KeyConstraint,
forwardPrimaryKey: SQLPrimaryKeyDescriptor)
throws -> SQLTableGenerator.KeyConstraint
{
try SQLTableGenerator.KeyConstraint(
columns: constraint.components.flatMap { component -> [String] in
switch component {
case let .columnName(columnName):
return [columnName]
case let .columnDefinition(column):
return [column.name]
case let .foreignKeyDefinition(foreignKey):
return try Self.makeForeignKeyGenerator(
db, foreignKey: foreignKey,
originTable: table.name,
forwardPrimaryKey: forwardPrimaryKey,
indexOptions: indexOptions).columnNames
}
},
conflictResolution: constraint.conflictResolution)
}
var forwardPrimaryKeyColumns: [SQLColumnDescriptor]?
if let primaryKeyConstraint = table.primaryKeyConstraint {
forwardPrimaryKeyColumns = try Self.forwardPrimaryKeyColumns(
db, primaryKeyConstraint: primaryKeyConstraint,
originTable: table.name)
} else {
for component in table.columnComponents {
if case let .columnDefinition(column) = component, column.primaryKey != nil {
forwardPrimaryKeyColumns = [SQLColumnDescriptor(column)]
break
}
}
}
let forwardPrimaryKey = SQLPrimaryKeyDescriptor(
tableName: table.name,
primaryKeyColumns: forwardPrimaryKeyColumns)
var columnGenerators: [SQLColumnGenerator] = []
var foreignKeyConstraints: [SQLForeignKeyConstraint] = []
var indexGenerators: [SQLIndexGenerator] = []
for component in table.columnComponents {
switch component {
case let .columnDefinition(column):
columnGenerators.append(.columnDefinition(column))
case let .columnLiteral(sql):
columnGenerators.append(.columnLiteral(sql))
case let .foreignKeyDefinition(foreignKey):
let fkGenerator = try Self.makeForeignKeyGenerator(
db, foreignKey: foreignKey,
originTable: table.name,
forwardPrimaryKey: forwardPrimaryKey,
indexOptions: indexOptions)
columnGenerators.append(contentsOf: fkGenerator.columnGenerators)
if let indexGenerator = fkGenerator.indexGenerator {
indexGenerators.append(indexGenerator)
}
if let foreignKeyConstraint = fkGenerator.foreignKeyConstraint {
foreignKeyConstraints.append(foreignKeyConstraint)
}
case let .foreignKeyConstraint(constraint):
foreignKeyConstraints.append(constraint)
}
}
for columnGenerator in columnGenerators {
if case let .columnDefinition(column) = columnGenerator,
let index = column.indexDefinition(in: table.name, options: indexOptions)
{
indexGenerators.append(SQLIndexGenerator(index: index))
}
}
try self.init(
name: table.name,
options: table.options,
columnGenerators: columnGenerators,
forwardPrimaryKeyColumns: forwardPrimaryKeyColumns,
primaryKeyConstraint: table.primaryKeyConstraint.map {
try makeKeyConstraint(db, constraint: $0, forwardPrimaryKey: forwardPrimaryKey)
},
uniqueKeyConstraints: table.uniqueKeyConstraints.map {
try makeKeyConstraint(db, constraint: $0, forwardPrimaryKey: forwardPrimaryKey)
},
foreignKeyConstraints: foreignKeyConstraints,
checkConstraints: table.checkConstraints,
literalConstraints: table.literalConstraints,
indexGenerators: indexGenerators)
}
private static func forwardPrimaryKeyColumns(
_ db: Database,
primaryKeyConstraint: TableDefinition.KeyConstraint,
originTable: String)
throws -> [SQLColumnDescriptor]?
{
var forwardPrimaryKeyColumns: [SQLColumnDescriptor] = []
for component in primaryKeyConstraint.components {
switch component {
case let .columnDefinition(column):
forwardPrimaryKeyColumns.append(SQLColumnDescriptor(column))
case let .foreignKeyDefinition(foreignKey):
let fkGenerator = try makeForeignKeyGenerator(
db, foreignKey: foreignKey,
originTable: originTable,
forwardPrimaryKey: nil, // not known yet, since we're building it
indexOptions: [])
for columnGenerator in fkGenerator.columnGenerators {
switch columnGenerator {
case let .columnDefinition(column):
forwardPrimaryKeyColumns.append(SQLColumnDescriptor(column))
case .columnLiteral:
// Unknown column name
return nil
}
}
case let .columnName(name):
forwardPrimaryKeyColumns.append(SQLColumnDescriptor(name: name, type: nil))
}
}
return forwardPrimaryKeyColumns
}
private static func makeForeignKeyGenerator(
_ db: Database,
foreignKey: ForeignKeyDefinition,
originTable: String,
forwardPrimaryKey: SQLPrimaryKeyDescriptor?,
indexOptions: IndexOptions)
throws -> ForeignKeyGenerator
{
let destinationPrimaryKey: SQLPrimaryKeyDescriptor
if let table = foreignKey.table {
if let forwardPrimaryKey,
originTable.lowercased() == table.lowercased()
{
// autoreference
destinationPrimaryKey = forwardPrimaryKey
} else {
destinationPrimaryKey = try foreignKey.primaryKey(db)
}
} else {
if let forwardPrimaryKey,
originTable.singularized.lowercased() == foreignKey.name.singularized.lowercased()
{
// autoreference
destinationPrimaryKey = forwardPrimaryKey
} else {
destinationPrimaryKey = try foreignKey.primaryKey(db)
}
}
guard let primaryKeyColumns = destinationPrimaryKey.primaryKeyColumns else {
// Destination table has an hidden rowID primary key
let columnName = foreignKey.name + "Id"
let column = ColumnDefinition(name: columnName, type: .integer).references(
destinationPrimaryKey.tableName,
onDelete: foreignKey.deleteAction,
onUpdate: foreignKey.updateAction,
deferred: foreignKey.isDeferred)
if let notNullConflictResolution = foreignKey.notNullConflictResolution {
column.notNull(onConflict: notNullConflictResolution)
}
switch foreignKey.indexing {
case nil:
break
case .index:
column.indexed()
case .unique:
column.unique()
}
return ForeignKeyGenerator(
columnNames: [columnName],
columnGenerators: [SQLColumnGenerator.columnDefinition(column)],
foreignKeyConstraint: nil,
indexGenerator: nil)
}
assert(!primaryKeyColumns.isEmpty)
let columnNames = primaryKeyColumns.map {
foreignKey.name + $0.name.uppercasingFirstCharacter
}
if primaryKeyColumns.count == 1 {
// Destination table has a single column primary key
let pkColumn = primaryKeyColumns[0]
let columnName = columnNames[0]
let column = ColumnDefinition(name: columnName, type: pkColumn.type).references(
destinationPrimaryKey.tableName,
column: pkColumn.name,
onDelete: foreignKey.deleteAction,
onUpdate: foreignKey.updateAction,
deferred: foreignKey.isDeferred)
if let notNullConflictResolution = foreignKey.notNullConflictResolution {
column.notNull(onConflict: notNullConflictResolution)
}
switch foreignKey.indexing {
case nil:
break
case .index:
column.indexed()
case .unique:
column.unique()
}
return ForeignKeyGenerator(
columnNames: [columnName],
columnGenerators: [SQLColumnGenerator.columnDefinition(column)],
foreignKeyConstraint: nil,
indexGenerator: nil)
} else {
// Destination table has a composite primary key
let columnGenerators = zip(primaryKeyColumns, columnNames).map { pkColumn, columnName in
let column = ColumnDefinition(name: columnName, type: pkColumn.type)
if let notNullConflictResolution = foreignKey.notNullConflictResolution {
column.notNull(onConflict: notNullConflictResolution)
}
return SQLColumnGenerator.columnDefinition(column)
}
let foreignKeyConstraint = SQLForeignKeyConstraint(
columns: columnNames,
destinationTable: destinationPrimaryKey.tableName,
destinationColumns: nil,
deleteAction: foreignKey.deleteAction,
updateAction: foreignKey.updateAction,
isDeferred: foreignKey.isDeferred)
let indexGenerator: SQLIndexGenerator?
switch foreignKey.indexing {
case nil:
indexGenerator = nil
case .index:
indexGenerator = SQLIndexGenerator(
name: Database.defaultIndexName(on: originTable, columns: columnNames),
table: originTable,
expressions: columnNames.map { .column($0) },
options: indexOptions,
condition: nil)
case .unique:
indexGenerator = SQLIndexGenerator(
name: Database.defaultIndexName(on: originTable, columns: columnNames),
table: originTable,
expressions: columnNames.map { .column($0) },
options: indexOptions.union([.unique]),
condition: nil)
}
return ForeignKeyGenerator(
columnNames: columnNames,
columnGenerators: columnGenerators,
foreignKeyConstraint: foreignKeyConstraint,
indexGenerator: indexGenerator)
}
}
}
struct SQLColumnDescriptor {
static let rowID = SQLColumnDescriptor(name: Column.rowID.name, type: .integer)
var name: String
var type: Database.ColumnType?
}
extension SQLColumnDescriptor {
init(_ column: ColumnInfo) {
self.init(
name: column.name,
type: column.columnType)
}
init(_ column: ColumnDefinition) {
self.init(name: column.name, type: column.type)
}
}
struct SQLForeignKeyConstraint {
var columns: [String]
var destinationTable: String
var destinationColumns: [String]?
var deleteAction: Database.ForeignKeyAction?
var updateAction: Database.ForeignKeyAction?
var isDeferred: Bool
}
struct SQLPrimaryKeyDescriptor {
/// The name of the forward-declared table
var tableName: String
/// If nil, the primary key is the hidden rowID.
var primaryKeyColumns: [SQLColumnDescriptor]?
}
extension SQLPrimaryKeyDescriptor {
static func find(_ db: Database, table: String) throws -> Self {
let columnInfos = try db.primaryKey(table).columnInfos
return SQLPrimaryKeyDescriptor(
tableName: table,
primaryKeyColumns: columnInfos.map { columnInfos in
columnInfos.map { SQLColumnDescriptor($0) }
})
}
}