Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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+
publiclet 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+
publicinit(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)

AltStyle によって変換されたページ (->オリジナル) /