Skip to content

Commit

Permalink
Merge pull request #1148 from stephencelis/schemachanger-part-2
Browse files Browse the repository at this point in the history
schemachanger-part-2
  • Loading branch information
jberkel authored Oct 18, 2022
2 parents 2181f48 + 020ec7a commit a4c1d1d
Show file tree
Hide file tree
Showing 20 changed files with 966 additions and 367 deletions.
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ disabled_rules: # rule identifiers to exclude from running
- operator_whitespace
- large_tuple
- closure_parameter_position
- inclusive_language # sqlite_master etc.
included: # paths to include during linting. `--path` is ignored if present. takes precendence over `excluded`.
- Sources
- Tests
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
0.14.0 (tbd), [diff][diff-0.14.0]
========================================

* Support more complex schema changes and queries ([#1073][], [#1146][] [#1148][])
* Support `ATTACH`/`DETACH` ([#30][], [#1142][])
* Support `WITH` clause ([#1139][])
* Add `Value` conformance for `NSURL` ([#1110][], [#1141][])
Expand Down Expand Up @@ -160,6 +161,7 @@
[#866]: https://github.com/stephencelis/SQLite.swift/pull/866
[#881]: https://github.com/stephencelis/SQLite.swift/pull/881
[#919]: https://github.com/stephencelis/SQLite.swift/pull/919
[#1073]: https://github.com/stephencelis/SQLite.swift/issues/1073
[#1075]: https://github.com/stephencelis/SQLite.swift/pull/1075
[#1077]: https://github.com/stephencelis/SQLite.swift/issues/1077
[#1094]: https://github.com/stephencelis/SQLite.swift/pull/1094
Expand All @@ -185,3 +187,5 @@
[#1141]: https://github.com/stephencelis/SQLite.swift/pull/1141
[#1142]: https://github.com/stephencelis/SQLite.swift/pull/1142
[#1144]: https://github.com/stephencelis/SQLite.swift/pull/1144
[#1146]: https://github.com/stephencelis/SQLite.swift/pull/1146
[#1148]: https://github.com/stephencelis/SQLite.swift/pull/1148
82 changes: 78 additions & 4 deletions Documentation/Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,37 @@ try db.transaction {

> _Note:_ Transactions run in a serial queue.

## Querying the Schema

We can obtain generic information about objects in the current schema with a `SchemaReader`:

```swift
let schema = db.schema
```

To query the data:

```swift
let indexes = try schema.objectDefinitions(type: .index)
let tables = try schema.objectDefinitions(type: .table)
let triggers = try schema.objectDefinitions(type: .trigger)
```

### Indexes and Columns

Specialized methods are available to get more detailed information:

```swift
let indexes = try schema.indexDefinitions("users")
let columns = try schema.columnDefinitions("users")

for index in indexes {
print("\(index.name) columns:\(index.columns))")
}
for column in columns {
print("\(column.name) pk:\(column.primaryKey) nullable: \(column.nullable)")
}
```

## Altering the Schema

Expand Down Expand Up @@ -1455,11 +1486,56 @@ tables](#creating-a-table).

### Renaming Columns

Added in SQLite 3.25.0, not exposed yet. [#1073](https://github.com/stephencelis/SQLite.swift/issues/1073)
We can rename columns with the help of the `SchemaChanger` class:

```swift
let schemaChanger = SchemaChanger(connection: db)
try schemaChanger.alter(table: "users") { table in
table.rename(column: "old_name", to: "new_name")
}
```

### Dropping Columns

Added in SQLite 3.35.0, not exposed yet. [#1073](https://github.com/stephencelis/SQLite.swift/issues/1073)
```swift
let schemaChanger = SchemaChanger(connection: db)
try schemaChanger.alter(table: "users") { table in
table.drop(column: "email")
}
```

These operations will work with all versions of SQLite and use modern SQL
operations such as `DROP COLUMN` when available.

### Adding Columns (SchemaChanger)

The `SchemaChanger` provides an alternative API to add new columns:

```swift
let newColumn = ColumnDefinition(
name: "new_text_column",
type: .TEXT,
nullable: true,
defaultValue: .stringLiteral("foo")
)

let schemaChanger = SchemaChanger(connection: db)

try schemaChanger.alter(table: "users") { table in
table.add(newColumn)
}
```

### Renaming/dropping Tables (SchemaChanger)

The `SchemaChanger` provides an alternative API to rename and drop tables:

```swift
let schemaChanger = SchemaChanger(connection: db)

try schemaChanger.rename(table: "users", to: "users_new")
try schemaChanger.drop(table: "emails")
```

### Indexes

Expand Down Expand Up @@ -1516,7 +1592,6 @@ try db.run(users.dropIndex(email, ifExists: true))
// DROP INDEX IF EXISTS "index_users_on_email"
```


### Dropping Tables

We can build
Expand All @@ -1536,7 +1611,6 @@ try db.run(users.drop(ifExists: true))
// DROP TABLE IF EXISTS "users"
```


### Migrations and Schema Versioning

You can use the convenience property on `Connection` to query and set the
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ build:

lint:
swiftlint --strict
lint-fix:
swiftlint lint fix

test:
ifdef XCPRETTY
Expand Down
28 changes: 28 additions & 0 deletions SQLite.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,31 @@ db.createAggregation("customConcat",
result: { $0 })
let result = try db.prepare("SELECT customConcat(email) FROM users").scalar() as! String
print(result)

/// schema queries
let schema = db.schema
let objects = try schema.objectDefinitions()
print(objects)

let columns = try schema.columnDefinitions(table: "users")
print(columns)

/// schema alteration

let schemaChanger = SchemaChanger(connection: db)
try schemaChanger.alter(table: "users") { table in
table.add(ColumnDefinition(name: "age", type: .INTEGER))
table.rename(column: "email", to: "electronic_mail")
table.drop(column: "name")
}

let changedColumns = try schema.columnDefinitions(table: "users")
print(changedColumns)

let age = Expression<Int?>("age")
let electronicMail = Expression<String>("electronic_mail")

let newRowid = try db.run(users.insert(
electronicMail <- "carol@mac.com",
age <- 33
))
30 changes: 30 additions & 0 deletions SQLite.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@
997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; };
997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; };
997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */ = {isa = PBXBuildFile; fileRef = 997DF2AD287FC06D00F8DF95 /* Query+with.swift */; };
DB58B21128FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; };
DB58B21228FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; };
DB58B21328FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; };
DB58B21428FB864300F8EEA4 /* SchemaReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21028FB864300F8EEA4 /* SchemaReader.swift */; };
DB58B21628FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; };
DB58B21728FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; };
DB58B21828FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; };
DB58B21928FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */; };
DB7C5DA628D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; };
DB7C5DA728D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; };
DB7C5DA828D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; };
DB7C5DA928D7C9B6006395CF /* SQLiteVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */; };
EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; };
EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; };
EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; };
Expand Down Expand Up @@ -325,6 +337,9 @@
49EB68C31F7B3CB400D89D40 /* Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = "<group>"; };
997DF2AD287FC06D00F8DF95 /* Query+with.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+with.swift"; sourceTree = "<group>"; };
A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DB58B21028FB864300F8EEA4 /* SchemaReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaReader.swift; sourceTree = "<group>"; };
DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteFeature.swift; sourceTree = "<group>"; };
DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLiteVersion.swift; sourceTree = "<group>"; };
EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = "<group>"; };
EE247AD81C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand Down Expand Up @@ -424,6 +439,7 @@
19A1792D261C689FC988A90A /* Schema */ = {
isa = PBXGroup;
children = (
DB58B21028FB864300F8EEA4 /* SchemaReader.swift */,
19A171B262DDE8718513CFDA /* SchemaChanger.swift */,
19A17268AE67B746B96AC125 /* SchemaDefinitions.swift */,
19A170A97B51DC5EE365F3C5 /* Connection+Schema.swift */,
Expand Down Expand Up @@ -569,6 +585,8 @@
19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */,
3DF7B78728842972005DD8CA /* Connection+Attach.swift */,
3DF7B790288449BA005DD8CA /* URIQueryParameter.swift */,
DB58B21528FC7C4600F8EEA4 /* SQLiteFeature.swift */,
DB7C5DA528D7C9B6006395CF /* SQLiteVersion.swift */,
19A17F285B767BFACD96714B /* Connection+Pragmas.swift */,
);
path = Core;
Expand Down Expand Up @@ -954,13 +972,16 @@
02A43A9A22738CF100FEC494 /* Backup.swift in Sources */,
19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */,
19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */,
DB7C5DA828D7C9B6006395CF /* SQLiteVersion.swift in Sources */,
19A17073552293CA063BEA66 /* Result.swift in Sources */,
997DF2B0287FC06D00F8DF95 /* Query+with.swift in Sources */,
19A179B59450FE7C4811AB8A /* Connection+Aggregation.swift in Sources */,
DB58B21828FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */,
19A17FC07731779C1B8506FA /* SchemaChanger.swift in Sources */,
19A1740EACD47904AA24B8DC /* SchemaDefinitions.swift in Sources */,
19A1750EF4A5F92954A451FF /* Connection+Schema.swift in Sources */,
19A17986405D9A875698408F /* Connection+Pragmas.swift in Sources */,
DB58B21328FB864300F8EEA4 /* SchemaReader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1011,9 +1032,12 @@
997DF2B1287FC06D00F8DF95 /* Query+with.swift in Sources */,
3D67B3F71DB246D700A4F4C6 /* Foundation.swift in Sources */,
3D67B3F81DB246D700A4F4C6 /* Helpers.swift in Sources */,
DB58B21428FB864300F8EEA4 /* SchemaReader.swift in Sources */,
3D67B3E91DB246D100A4F4C6 /* Statement.swift in Sources */,
DB7C5DA928D7C9B6006395CF /* SQLiteVersion.swift in Sources */,
3D67B3EA1DB246D100A4F4C6 /* Value.swift in Sources */,
3D67B3EB1DB246D100A4F4C6 /* FTS4.swift in Sources */,
DB58B21928FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */,
3D67B3EC1DB246D100A4F4C6 /* RTree.swift in Sources */,
3D67B3ED1DB246D100A4F4C6 /* FTS5.swift in Sources */,
3D67B3EE1DB246D100A4F4C6 /* AggregateFunctions.swift in Sources */,
Expand Down Expand Up @@ -1069,13 +1093,16 @@
02A43A9822738CF100FEC494 /* Backup.swift in Sources */,
19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */,
19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */,
DB7C5DA628D7C9B6006395CF /* SQLiteVersion.swift in Sources */,
19A173EFEF0B3BD0B3ED406C /* Result.swift in Sources */,
997DF2AE287FC06D00F8DF95 /* Query+with.swift in Sources */,
19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */,
DB58B21628FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */,
19A1773A335CAB9D0AE14E8E /* SchemaChanger.swift in Sources */,
19A17BA13FD35F058787B7D3 /* SchemaDefinitions.swift in Sources */,
19A174506543905D71BF0518 /* Connection+Schema.swift in Sources */,
19A17018F250343BD0F9F4B0 /* Connection+Pragmas.swift in Sources */,
DB58B21128FB864300F8EEA4 /* SchemaReader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -1146,13 +1173,16 @@
02A43A9922738CF100FEC494 /* Backup.swift in Sources */,
19A17490543609FCED53CACC /* Errors.swift in Sources */,
19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */,
DB7C5DA728D7C9B6006395CF /* SQLiteVersion.swift in Sources */,
19A17F1B3F0A3C96B5ED6D64 /* Result.swift in Sources */,
997DF2AF287FC06D00F8DF95 /* Query+with.swift in Sources */,
19A170ACC97B19730FB7BA4D /* Connection+Aggregation.swift in Sources */,
DB58B21728FC7C4600F8EEA4 /* SQLiteFeature.swift in Sources */,
19A177290558991BCC60E4E3 /* SchemaChanger.swift in Sources */,
19A17B0DF1DDB6BBC9C95D64 /* SchemaDefinitions.swift in Sources */,
19A17F0BF02896E1664F4090 /* Connection+Schema.swift in Sources */,
19A1760CE25615CA015E2E5F /* Connection+Pragmas.swift in Sources */,
DB58B21228FB864300F8EEA4 /* SchemaReader.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
5 changes: 2 additions & 3 deletions Sources/SQLite/Core/Connection+Pragmas.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Foundation

public typealias UserVersion = Int32
public typealias SQLiteVersion = (Int, Int, Int)

public extension Connection {
/// The user version of the database.
Expand All @@ -21,9 +20,9 @@ public extension Connection {
guard let version = (try? scalar("SELECT sqlite_version()")) as? String,
let splits = .some(version.split(separator: ".", maxSplits: 3)), splits.count == 3,
let major = Int(splits[0]), let minor = Int(splits[1]), let point = Int(splits[2]) else {
return (0, 0, 0)
return .zero
}
return (major, minor, point)
return .init(major: major, minor: minor, point: point)
}

// Changing the foreign_keys setting affects the execution of all statements prepared using the database
Expand Down
25 changes: 25 additions & 0 deletions Sources/SQLite/Core/SQLiteFeature.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation

enum SQLiteFeature {
case partialIntegrityCheck // PRAGMA integrity_check(table)
case sqliteSchemaTable // sqlite_master => sqlite_schema
case renameColumn // ALTER TABLE ... RENAME COLUMN
case dropColumn // ALTER TABLE ... DROP COLUMN

func isSupported(by version: SQLiteVersion) -> Bool {
switch self {
case .partialIntegrityCheck, .sqliteSchemaTable:
return version >= .init(major: 3, minor: 33)
case .renameColumn:
return version >= .init(major: 3, minor: 25)
case .dropColumn:
return version >= .init(major: 3, minor: 35)
}
}
}

extension Connection {
func supports(_ feature: SQLiteFeature) -> Bool {
feature.isSupported(by: sqliteVersion)
}
}
22 changes: 22 additions & 0 deletions Sources/SQLite/Core/SQLiteVersion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation

public struct SQLiteVersion: Comparable, CustomStringConvertible {
public let major: Int
public let minor: Int
public var point: Int = 0

public var description: String {
"SQLite \(major).\(minor).\(point)"
}

public static func <(lhs: SQLiteVersion, rhs: SQLiteVersion) -> Bool {
lhs.tuple < rhs.tuple
}

public static func ==(lhs: SQLiteVersion, rhs: SQLiteVersion) -> Bool {
lhs.tuple == rhs.tuple
}

static var zero: SQLiteVersion = .init(major: 0, minor: 0)
private var tuple: (Int, Int, Int) { (major, minor, point) }
}
Loading

0 comments on commit a4c1d1d

Please sign in to comment.