add swiftUI code
This commit is contained in:
+145
@@ -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: " ")
|
||||
}
|
||||
}
|
||||
+567
@@ -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
|
||||
}
|
||||
}
|
||||
+42
@@ -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
|
||||
}
|
||||
}
|
||||
+825
@@ -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
|
||||
}
|
||||
}
|
||||
+84
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+476
@@ -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) }
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user