477 lines
20 KiB
Swift
477 lines
20 KiB
Swift
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) }
|
|
})
|
|
|
|
}
|
|
}
|