Skip to content

Commit a6e6e68

Browse files
committed
WIP
1 parent ba32ad7 commit a6e6e68

File tree

6 files changed

+119
-50
lines changed

6 files changed

+119
-50
lines changed

Sources/SQLite/Schema/Connection+Schema.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension Connection {
2424
return ColumnDefinition(name: name,
2525
primaryKey: primaryKey == 1 ? try parsePrimaryKey(column: name) : nil,
2626
type: ColumnDefinition.Affinity.from(type),
27-
null: notNull == 0,
27+
nullable: notNull == 0,
2828
defaultValue: .from(defaultValue),
2929
references: foreignKeys[name]?.first)
3030
}

Sources/SQLite/Schema/SchemaChanger.swift

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,36 +27,63 @@ import Foundation
2727
12. If foreign keys constraints were originally enabled, reenable them now.
2828
*/
2929
public class SchemaChanger: CustomStringConvertible {
30-
enum SchemaChangeError: LocalizedError {
30+
public enum Error: LocalizedError {
31+
case invalidColumnDefinition(String)
3132
case foreignKeyError([ForeignKeyError])
3233

33-
var errorDescription: String? {
34+
public var errorDescription: String? {
3435
switch self {
3536
case .foreignKeyError(let errors):
3637
return "Foreign key errors: \(errors)"
38+
case .invalidColumnDefinition(let message):
39+
return "Invalid column definition: \(message)"
3740
}
3841
}
3942
}
4043

4144
public enum Operation {
42-
case none
43-
case add(ColumnDefinition)
44-
case remove(String)
45+
case addColumn(ColumnDefinition)
46+
case dropColumn(String)
4547
case renameColumn(String, String)
4648
case renameTable(String)
4749

4850
/// Returns non-nil if the operation can be executed with a simple SQL statement
4951
func toSQL(_ table: String, version: SQLiteVersion) -> String? {
5052
switch self {
51-
case .add(let definition):
53+
case .addColumn(let definition):
5254
return "ALTER TABLE \(table.quote()) ADD COLUMN \(definition.toSQL())"
5355
case .renameColumn(let from, let to) where version >= (3, 25, 0):
5456
return "ALTER TABLE \(table.quote()) RENAME COLUMN \(from.quote()) TO \(to.quote())"
55-
case .remove(let column) where version >= (3, 35, 0):
57+
case .dropColumn(let column) where version >= (3, 35, 0):
5658
return "ALTER TABLE \(table.quote()) DROP COLUMN \(column.quote())"
5759
default: return nil
5860
}
5961
}
62+
63+
func validate() throws {
64+
switch self {
65+
case .addColumn(let definition):
66+
// The new column may take any of the forms permissible in a CREATE TABLE statement, with the following restrictions:
67+
// - The column may not have a PRIMARY KEY or UNIQUE constraint.
68+
// - The column may not have a default value of CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP, or an expression in parentheses
69+
// - If a NOT NULL constraint is specified, then the column must have a default value other than NULL.
70+
guard definition.primaryKey == nil else {
71+
throw Error.invalidColumnDefinition("can not add primary key column")
72+
}
73+
let invalidValues: [LiteralValue] = [.CURRENT_TIME, .CURRENT_DATE, .CURRENT_TIMESTAMP]
74+
if invalidValues.contains(definition.defaultValue) {
75+
throw Error.invalidColumnDefinition("Invalid default value")
76+
}
77+
if !definition.nullable && definition.defaultValue == .NULL {
78+
throw Error.invalidColumnDefinition("NOT NULL columns must have a default value other than NULL")
79+
}
80+
case .dropColumn:
81+
// The DROP COLUMN command only works if the column is not referenced by any other parts of the schema
82+
// and is not a PRIMARY KEY and does not have a UNIQUE constraint
83+
break
84+
default: break
85+
}
86+
}
6087
}
6188

6289
public class AlterTableDefinition {
@@ -69,11 +96,11 @@ public class SchemaChanger: CustomStringConvertible {
6996
}
7097

7198
public func add(_ column: ColumnDefinition) {
72-
operations.append(.add(column))
99+
operations.append(.addColumn(column))
73100
}
74101

75102
public func remove(_ column: String) {
76-
operations.append(.remove(column))
103+
operations.append(.dropColumn(column))
77104
}
78105

79106
public func rename(_ column: String, to: String) {
@@ -116,7 +143,15 @@ public class SchemaChanger: CustomStringConvertible {
116143
try dropTable(table)
117144
}
118145

146+
// Beginning with release 3.25.0 (2018-09-15), references to the table within trigger bodies and
147+
// view definitions are also renamed.
148+
public func rename(table: String, to: String) throws {
149+
try connection.run("ALTER TABLE \(table.quote()) RENAME TO \(to.quote())")
150+
}
151+
119152
private func run(table: String, operation: Operation) throws {
153+
try operation.validate()
154+
120155
if let sql = operation.toSQL(table, version: version) {
121156
try connection.run(sql)
122157
} else {
@@ -129,10 +164,10 @@ public class SchemaChanger: CustomStringConvertible {
129164
try disableRefIntegrity {
130165
let tempTable = "\(SchemaChanger.tempPrefix)\(table)"
131166
try moveTable(from: table, to: tempTable, options: [.temp], operation: operation)
132-
try moveTable(from: tempTable, to: table)
167+
try rename(table: tempTable, to: table)
133168
let foreignKeyErrors = try connection.foreignKeyCheck()
134169
if foreignKeyErrors.count > 0 {
135-
throw SchemaChangeError.foreignKeyError(foreignKeyErrors)
170+
throw Error.foreignKeyError(foreignKeyErrors)
136171
}
137172
}
138173
}
@@ -153,22 +188,24 @@ public class SchemaChanger: CustomStringConvertible {
153188
try block()
154189
}
155190

156-
private func moveTable(from: String, to: String, options: Options = .default, operation: Operation = .none) throws {
191+
private func moveTable(from: String, to: String, options: Options = .default, operation: Operation? = nil) throws {
157192
try copyTable(from: from, to: to, options: options, operation: operation)
158193
try dropTable(from)
159194
}
160195

161-
private func copyTable(from: String, to: String, options: Options = .default, operation: Operation) throws {
196+
private func copyTable(from: String, to: String, options: Options = .default, operation: Operation?) throws {
162197
let fromDefinition = TableDefinition(
163198
name: from,
164199
columns: try connection.columnInfo(table: from),
165200
indexes: try connection.indexInfo(table: from)
166201
)
167-
let toDefinition = fromDefinition.apply(.renameTable(to)).apply(operation)
202+
let toDefinition = fromDefinition
203+
.apply(.renameTable(to))
204+
.apply(operation)
168205

169206
try createTable(definition: toDefinition, options: options)
170207
try createTableIndexes(definition: toDefinition)
171-
if case .remove = operation {
208+
if case .dropColumn = operation {
172209
try copyTableContents(from: fromDefinition.apply(operation), to: toDefinition)
173210
} else {
174211
try copyTableContents(from: fromDefinition, to: toDefinition)
@@ -221,11 +258,11 @@ extension IndexDefinition {
221258
}
222259

223260
extension TableDefinition {
224-
func apply(_ operation: SchemaChanger.Operation) -> TableDefinition {
261+
func apply(_ operation: SchemaChanger.Operation?) -> TableDefinition {
225262
switch operation {
226263
case .none: return self
227-
case .add: fatalError("Use 'ALTER TABLE ADD COLUMN (...)'")
228-
case .remove(let column):
264+
case .addColumn: fatalError("Use 'ALTER TABLE ADD COLUMN (...)'")
265+
case .dropColumn(let column):
229266
return TableDefinition(name: name,
230267
columns: columns.filter { $0.name != column },
231268
indexes: indexes.filter { !$0.columns.contains(column) }

Sources/SQLite/Schema/SchemaDefinitions.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,23 +84,27 @@ public struct ColumnDefinition: Equatable {
8484
public let name: String
8585
public let primaryKey: PrimaryKey?
8686
public let type: Affinity
87-
public let null: Bool
87+
public let nullable: Bool
8888
public let defaultValue: LiteralValue
8989
public let references: ForeignKey?
9090

91-
public init(name: String, primaryKey: PrimaryKey?, type: Affinity, null: Bool, defaultValue: LiteralValue,
92-
references: ForeignKey?) {
91+
public init(name: String,
92+
primaryKey: PrimaryKey? = nil,
93+
type: Affinity,
94+
nullable: Bool = false,
95+
defaultValue: LiteralValue = .NULL,
96+
references: ForeignKey? = nil) {
9397
self.name = name
9498
self.primaryKey = primaryKey
9599
self.type = type
96-
self.null = null
100+
self.nullable = nullable
97101
self.defaultValue = defaultValue
98102
self.references = references
99103
}
100104

101105
func rename(from: String, to: String) -> ColumnDefinition {
102106
guard from == name else { return self }
103-
return ColumnDefinition(name: to, primaryKey: primaryKey, type: type, null: null, defaultValue: defaultValue, references: references)
107+
return ColumnDefinition(name: to, primaryKey: primaryKey, type: type, nullable: nullable, defaultValue: defaultValue, references: references)
104108
}
105109
}
106110

@@ -254,12 +258,12 @@ public struct IndexDefinition: Equatable {
254258
}
255259
}
256260

257-
struct ForeignKeyError: CustomStringConvertible {
261+
public struct ForeignKeyError: CustomStringConvertible {
258262
let from: String
259263
let rowId: Int64
260264
let to: String
261265

262-
var description: String {
266+
public var description: String {
263267
"\(from) [\(rowId)] => \(to)"
264268
}
265269
}
@@ -294,7 +298,7 @@ extension ColumnDefinition {
294298
type.rawValue,
295299
defaultValue.map { "DEFAULT \($0)" },
296300
primaryKey.map { $0.toSQL() },
297-
null ? nil : "NOT NULL",
301+
nullable ? nil : "NOT NULL",
298302
references.map { $0.toSQL() }
299303
].compactMap { $0 }
300304
.joined(separator: " ")

Tests/SQLiteTests/Schema/Connection+SchemaTests.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,42 @@ class ConnectionSchemaTests: SQLiteTestCase {
1414
ColumnDefinition(name: "id",
1515
primaryKey: .init(autoIncrement: false, onConflict: nil),
1616
type: .INTEGER,
17-
null: true,
17+
nullable: true,
1818
defaultValue: .NULL,
1919
references: nil),
2020
ColumnDefinition(name: "email",
2121
primaryKey: nil,
2222
type: .TEXT,
23-
null: false,
23+
nullable: false,
2424
defaultValue: .NULL,
2525
references: nil),
2626
ColumnDefinition(name: "age",
2727
primaryKey: nil,
2828
type: .INTEGER,
29-
null: true,
29+
nullable: true,
3030
defaultValue: .NULL,
3131
references: nil),
3232
ColumnDefinition(name: "salary",
3333
primaryKey: nil,
3434
type: .REAL,
35-
null: true,
35+
nullable: true,
3636
defaultValue: .NULL,
3737
references: nil),
3838
ColumnDefinition(name: "admin",
3939
primaryKey: nil,
4040
type: .TEXT,
41-
null: false,
41+
nullable: false,
4242
defaultValue: .numericLiteral("0"),
4343
references: nil),
4444
ColumnDefinition(name: "manager_id",
4545
primaryKey: nil, type: .INTEGER,
46-
null: true,
46+
nullable: true,
4747
defaultValue: .NULL,
4848
references: .init(table: "users", column: "manager_id", primaryKey: "id", onUpdate: nil, onDelete: nil)),
4949
ColumnDefinition(name: "created_at",
5050
primaryKey: nil,
5151
type: .TEXT,
52-
null: true,
52+
nullable: true,
5353
defaultValue: .NULL,
5454
references: nil)
5555
])
@@ -64,7 +64,7 @@ class ConnectionSchemaTests: SQLiteTestCase {
6464
name: "id",
6565
primaryKey: .init(autoIncrement: true, onConflict: .IGNORE),
6666
type: .INTEGER,
67-
null: true,
67+
nullable: true,
6868
defaultValue: .NULL,
6969
references: nil)
7070
]
@@ -80,7 +80,7 @@ class ConnectionSchemaTests: SQLiteTestCase {
8080
name: "id",
8181
primaryKey: .init(autoIncrement: false),
8282
type: .INTEGER,
83-
null: true,
83+
nullable: true,
8484
defaultValue: .NULL,
8585
references: nil)
8686
]

Tests/SQLiteTests/Schema/SchemaChangerTests.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,36 @@ class SchemaChangerTests: SQLiteTestCase {
9090
}
9191

9292
func test_add_column() throws {
93-
let newColumn = ColumnDefinition(name: "new_column", primaryKey: nil, type: .TEXT, null: true, defaultValue: .NULL, references: nil)
93+
let column = Expression<String>("new_column")
94+
let newColumn = ColumnDefinition(name: "new_column",
95+
type: .TEXT,
96+
nullable: true,
97+
defaultValue: .stringLiteral("foo"))
9498

9599
try schemaChanger.alter(table: "users") { table in
96100
table.add(newColumn)
97101
}
98102

99103
let columns = try db.columnInfo(table: "users")
100104
XCTAssertTrue(columns.contains(newColumn))
105+
106+
XCTAssertEqual(try db.pluck(users.select(column))?[column], "foo")
107+
}
108+
109+
func test_add_column_primary_key_fails() throws {
110+
let newColumn = ColumnDefinition(name: "new_column",
111+
primaryKey: .init(autoIncrement: false, onConflict: nil),
112+
type: .TEXT)
113+
114+
XCTAssertThrowsError(try schemaChanger.alter(table: "users") { table in
115+
table.add(newColumn)
116+
}) { error in
117+
if case SchemaChanger.Error.invalidColumnDefinition(_) = error {
118+
XCTAssertEqual("Invalid column definition: can not add primary key column", error.localizedDescription)
119+
} else {
120+
XCTFail("invalid error: \(error)")
121+
}
122+
}
101123
}
102124

103125
func test_drop_table() throws {
@@ -110,4 +132,10 @@ class SchemaChangerTests: SQLiteTestCase {
110132
}
111133
}
112134
}
135+
136+
func test_rename_table() throws {
137+
try schemaChanger.rename(table: "users", to: "users_new")
138+
let users_new = Table("users_new")
139+
XCTAssertEqual((try db.scalar(users_new.count)) as Int, 1)
140+
}
113141
}

0 commit comments

Comments
 (0)