Skip to content

Commit 870ea89

Browse files
authored
Merge pull request #1315 from stephencelis/schemachanger-create-table
Support creating tables in schema changer
2 parents 43eb040 + 25bd063 commit 870ea89

File tree

8 files changed

+453
-37
lines changed

8 files changed

+453
-37
lines changed

Documentation/Index.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
- [Renaming Columns](#renaming-columns)
6464
- [Dropping Columns](#dropping-columns)
6565
- [Renaming/Dropping Tables](#renamingdropping-tables)
66+
- [Creating Tables](#creating-tables)
6667
- [Indexes](#indexes)
6768
- [Creating Indexes](#creating-indexes)
6869
- [Dropping Indexes](#dropping-indexes)
@@ -1583,6 +1584,16 @@ try schemaChanger.rename(table: "users", to: "users_new")
15831584
try schemaChanger.drop(table: "emails", ifExists: false)
15841585
```
15851586

1587+
#### Creating Tables
1588+
1589+
```swift
1590+
let schemaChanger = SchemaChanger(connection: db)
1591+
1592+
try schemaChanger.create(table: "users") { table in
1593+
table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER))
1594+
table.add(column: .init(name: "name", type: .TEXT, nullable: false))
1595+
}
1596+
15861597
### Indexes
15871598

15881599

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ lint: $(SWIFTLINT)
3535
$< --strict
3636

3737
lint-fix: $(SWIFTLINT)
38-
$< lint fix
38+
$< --fix
3939

4040
clean:
4141
$(XCODEBUILD) $(BUILD_ARGUMENTS) clean

Sources/SQLite/Schema/SchemaChanger.swift

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,30 @@ public class SchemaChanger: CustomStringConvertible {
4343

4444
public enum Operation {
4545
case addColumn(ColumnDefinition)
46+
case addIndex(IndexDefinition, ifNotExists: Bool)
4647
case dropColumn(String)
48+
case dropIndex(String, ifExists: Bool)
4749
case renameColumn(String, String)
4850
case renameTable(String)
51+
case createTable(columns: [ColumnDefinition], ifNotExists: Bool)
4952

5053
/// Returns non-nil if the operation can be executed with a simple SQL statement
5154
func toSQL(_ table: String, version: SQLiteVersion) -> String? {
5255
switch self {
5356
case .addColumn(let definition):
5457
return "ALTER TABLE \(table.quote()) ADD COLUMN \(definition.toSQL())"
58+
case .addIndex(let definition, let ifNotExists):
59+
return definition.toSQL(ifNotExists: ifNotExists)
5560
case .renameColumn(let from, let to) where SQLiteFeature.renameColumn.isSupported(by: version):
5661
return "ALTER TABLE \(table.quote()) RENAME COLUMN \(from.quote()) TO \(to.quote())"
5762
case .dropColumn(let column) where SQLiteFeature.dropColumn.isSupported(by: version):
5863
return "ALTER TABLE \(table.quote()) DROP COLUMN \(column.quote())"
64+
case .dropIndex(let name, let ifExists):
65+
return "DROP INDEX \(ifExists ? " IF EXISTS " : "") \(name.quote())"
66+
case .createTable(let columns, let ifNotExists):
67+
return "CREATE TABLE \(ifNotExists ? " IF NOT EXISTS " : "") \(table.quote()) (" +
68+
columns.map { $0.toSQL() }.joined(separator: ", ") +
69+
")"
5970
default: return nil
6071
}
6172
}
@@ -89,7 +100,7 @@ public class SchemaChanger: CustomStringConvertible {
89100
public class AlterTableDefinition {
90101
fileprivate var operations: [Operation] = []
91102

92-
let name: String
103+
public let name: String
93104

94105
init(name: String) {
95106
self.name = name
@@ -99,21 +110,73 @@ public class SchemaChanger: CustomStringConvertible {
99110
operations.append(.addColumn(column))
100111
}
101112

113+
public func add(index: IndexDefinition, ifNotExists: Bool = false) {
114+
operations.append(.addIndex(index, ifNotExists: ifNotExists))
115+
}
116+
102117
public func drop(column: String) {
103118
operations.append(.dropColumn(column))
104119
}
105120

121+
public func drop(index: String, ifExists: Bool = false) {
122+
operations.append(.dropIndex(index, ifExists: ifExists))
123+
}
124+
106125
public func rename(column: String, to: String) {
107126
operations.append(.renameColumn(column, to))
108127
}
109128
}
110129

130+
public class CreateTableDefinition {
131+
fileprivate var columnDefinitions: [ColumnDefinition] = []
132+
fileprivate var indexDefinitions: [IndexDefinition] = []
133+
134+
let name: String
135+
let ifNotExists: Bool
136+
137+
init(name: String, ifNotExists: Bool) {
138+
self.name = name
139+
self.ifNotExists = ifNotExists
140+
}
141+
142+
public func add(column: ColumnDefinition) {
143+
columnDefinitions.append(column)
144+
}
145+
146+
public func add<T>(expression: Expression<T>) where T: Value {
147+
add(column: .init(name: columnName(for: expression), type: .init(expression: expression), nullable: false))
148+
}
149+
150+
public func add<T>(expression: Expression<T?>) where T: Value {
151+
add(column: .init(name: columnName(for: expression), type: .init(expression: expression), nullable: true))
152+
}
153+
154+
public func add(index: IndexDefinition) {
155+
indexDefinitions.append(index)
156+
}
157+
158+
var operations: [Operation] {
159+
precondition(!columnDefinitions.isEmpty)
160+
return [
161+
.createTable(columns: columnDefinitions, ifNotExists: ifNotExists)
162+
] + indexDefinitions.map { .addIndex($0, ifNotExists: ifNotExists) }
163+
}
164+
165+
private func columnName<T>(for expression: Expression<T>) -> String {
166+
switch LiteralValue(expression.template) {
167+
case .stringLiteral(let string): return string
168+
default: fatalError("expression is not a literal string value")
169+
}
170+
}
171+
}
172+
111173
private let connection: Connection
112174
private let schemaReader: SchemaReader
113175
private let version: SQLiteVersion
114176
static let tempPrefix = "tmp_"
115177
typealias Block = () throws -> Void
116178
public typealias AlterTableDefinitionBlock = (AlterTableDefinition) -> Void
179+
public typealias CreateTableDefinitionBlock = (CreateTableDefinition) -> Void
117180

118181
struct Options: OptionSet {
119182
let rawValue: Int
@@ -141,6 +204,15 @@ public class SchemaChanger: CustomStringConvertible {
141204
}
142205
}
143206

207+
public func create(table: String, ifNotExists: Bool = false, block: CreateTableDefinitionBlock) throws {
208+
let createTableDefinition = CreateTableDefinition(name: table, ifNotExists: ifNotExists)
209+
block(createTableDefinition)
210+
211+
for operation in createTableDefinition.operations {
212+
try run(table: table, operation: operation)
213+
}
214+
}
215+
144216
public func drop(table: String, ifExists: Bool = true) throws {
145217
try dropTable(table, ifExists: ifExists)
146218
}
@@ -151,6 +223,12 @@ public class SchemaChanger: CustomStringConvertible {
151223
try connection.run("ALTER TABLE \(table.quote()) RENAME TO \(to.quote())")
152224
}
153225

226+
// Runs arbitrary SQL. Should only be used if no predefined operations exist.
227+
@discardableResult
228+
public func run(_ sql: String, _ bindings: Binding?...) throws -> Statement {
229+
return try connection.run(sql, bindings)
230+
}
231+
154232
private func run(table: String, operation: Operation) throws {
155233
try operation.validate()
156234

@@ -263,7 +341,9 @@ extension TableDefinition {
263341
func apply(_ operation: SchemaChanger.Operation?) -> TableDefinition {
264342
switch operation {
265343
case .none: return self
344+
case .createTable, .addIndex, .dropIndex: fatalError()
266345
case .addColumn: fatalError("Use 'ALTER TABLE ADD COLUMN (...)'")
346+
267347
case .dropColumn(let column):
268348
return TableDefinition(name: name,
269349
columns: columns.filter { $0.name != column },
@@ -280,3 +360,13 @@ extension TableDefinition {
280360
}
281361
}
282362
}
363+
364+
extension ColumnDefinition.Affinity {
365+
init<T>(expression: Expression<T>) where T: Value {
366+
self.init(T.declaredDatatype)
367+
}
368+
369+
init<T>(expression: Expression<T?>) where T: Value {
370+
self.init(T.declaredDatatype)
371+
}
372+
}

Sources/SQLite/Schema/SchemaDefinitions.swift

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public struct ColumnDefinition: Equatable {
9393
// swiftlint:disable:next force_try
9494
static let pattern = try! NSRegularExpression(pattern: "PRIMARY KEY\\s*(?:ASC|DESC)?\\s*(?:ON CONFLICT (\\w+)?)?\\s*(AUTOINCREMENT)?")
9595

96-
init(autoIncrement: Bool = true, onConflict: OnConflict? = nil) {
96+
public init(autoIncrement: Bool = true, onConflict: OnConflict? = nil) {
9797
self.autoIncrement = autoIncrement
9898
self.onConflict = onConflict
9999
}
@@ -117,30 +117,46 @@ public struct ColumnDefinition: Equatable {
117117
}
118118

119119
public struct ForeignKey: Equatable {
120-
let table: String
121-
let column: String
122-
let primaryKey: String?
120+
let fromColumn: String
121+
let toTable: String
122+
// when null, use primary key of "toTable"
123+
let toColumn: String?
123124
let onUpdate: String?
124125
let onDelete: String?
126+
127+
public init(toTable: String, toColumn: String? = nil, onUpdate: String? = nil, onDelete: String? = nil) {
128+
self.init(fromColumn: "", toTable: toTable, toColumn: toColumn, onUpdate: onUpdate, onDelete: onDelete)
129+
}
130+
131+
public init(fromColumn: String, toTable: String, toColumn: String? = nil, onUpdate: String? = nil, onDelete: String? = nil) {
132+
self.fromColumn = fromColumn
133+
self.toTable = toTable
134+
self.toColumn = toColumn
135+
self.onUpdate = onUpdate
136+
self.onDelete = onDelete
137+
}
125138
}
126139

127140
public let name: String
128141
public let primaryKey: PrimaryKey?
129142
public let type: Affinity
130143
public let nullable: Bool
144+
public let unique: Bool
131145
public let defaultValue: LiteralValue
132146
public let references: ForeignKey?
133147

134148
public init(name: String,
135149
primaryKey: PrimaryKey? = nil,
136150
type: Affinity,
137151
nullable: Bool = true,
152+
unique: Bool = false,
138153
defaultValue: LiteralValue = .NULL,
139154
references: ForeignKey? = nil) {
140155
self.name = name
141156
self.primaryKey = primaryKey
142157
self.type = type
143158
self.nullable = nullable
159+
self.unique = unique
144160
self.defaultValue = defaultValue
145161
self.references = references
146162
}
@@ -244,16 +260,18 @@ public struct IndexDefinition: Equatable {
244260

245261
public enum Order: String { case ASC, DESC }
246262

247-
public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil, orders: [String: Order]? = nil) {
263+
public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil,
264+
orders: [String: Order]? = nil, origin: Origin? = nil) {
248265
self.table = table
249266
self.name = name
250267
self.unique = unique
251268
self.columns = columns
252269
self.where = `where`
253270
self.orders = orders
271+
self.origin = origin
254272
}
255273

256-
init (table: String, name: String, unique: Bool, columns: [String], indexSQL: String?) {
274+
init (table: String, name: String, unique: Bool, columns: [String], indexSQL: String?, origin: Origin? = nil) {
257275
func wherePart(sql: String) -> String? {
258276
IndexDefinition.whereRe.firstMatch(in: sql, options: [], range: NSRange(location: 0, length: sql.count)).map {
259277
(sql as NSString).substring(with: $0.range(at: 1))
@@ -270,12 +288,16 @@ public struct IndexDefinition: Equatable {
270288
return memo2
271289
}
272290
}
291+
292+
let orders = indexSQL.flatMap(orders)
293+
273294
self.init(table: table,
274295
name: name,
275296
unique: unique,
276297
columns: columns,
277298
where: indexSQL.flatMap(wherePart),
278-
orders: indexSQL.flatMap(orders))
299+
orders: (orders?.isEmpty ?? false) ? nil : orders,
300+
origin: origin)
279301
}
280302

281303
public let table: String
@@ -284,6 +306,13 @@ public struct IndexDefinition: Equatable {
284306
public let columns: [String]
285307
public let `where`: String?
286308
public let orders: [String: Order]?
309+
public let origin: Origin?
310+
311+
public enum Origin: String {
312+
case uniqueConstraint = "u" // index created from a "CREATE TABLE (... UNIQUE)" column constraint
313+
case createIndex = "c" // index created explicitly via "CREATE INDEX ..."
314+
case primaryKey = "pk" // index created from a "CREATE TABLE PRIMARY KEY" column constraint
315+
}
287316

288317
enum IndexError: LocalizedError {
289318
case tooLong(String, String)
@@ -297,6 +326,13 @@ public struct IndexDefinition: Equatable {
297326
}
298327
}
299328

329+
// Indices with names of the form "sqlite_autoindex_TABLE_N" that are used to implement UNIQUE and PRIMARY KEY
330+
// constraints on ordinary tables.
331+
// https://sqlite.org/fileformat2.html#intschema
332+
var isInternal: Bool {
333+
name.starts(with: "sqlite_autoindex_")
334+
}
335+
300336
func validate() throws {
301337
if name.count > IndexDefinition.maxIndexLength {
302338
throw IndexError.tooLong(name, table)
@@ -345,6 +381,7 @@ extension ColumnDefinition {
345381
defaultValue.map { "DEFAULT \($0)" },
346382
primaryKey.map { $0.toSQL() },
347383
nullable ? nil : "NOT NULL",
384+
unique ? "UNIQUE" : nil,
348385
references.map { $0.toSQL() }
349386
].compactMap { $0 }
350387
.joined(separator: " ")
@@ -376,8 +413,8 @@ extension ColumnDefinition.ForeignKey {
376413
func toSQL() -> String {
377414
([
378415
"REFERENCES",
379-
table.quote(),
380-
primaryKey.map { "(\($0.quote()))" },
416+
toTable.quote(),
417+
toColumn.map { "(\($0.quote()))" },
381418
onUpdate.map { "ON UPDATE \($0)" },
382419
onDelete.map { "ON DELETE \($0)" }
383420
] as [String?]).compactMap { $0 }

0 commit comments

Comments
 (0)