Skip to content

Commit ba32ad7

Browse files
authored
Merge pull request #1146 from stephencelis/schema-changer
Adds SchemaChanger to perform database schema changes
2 parents c94aca2 + f57c225 commit ba32ad7

34 files changed

+1753
-247
lines changed

SQLite.xcodeproj/project.pbxproj

Lines changed: 304 additions & 184 deletions
Large diffs are not rendered by default.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Foundation
2+
3+
public typealias UserVersion = Int32
4+
public typealias SQLiteVersion = (Int, Int, Int)
5+
6+
public extension Connection {
7+
/// The user version of the database.
8+
/// See SQLite [PRAGMA user_version](https://sqlite.org/pragma.html#pragma_user_version)
9+
var userVersion: UserVersion? {
10+
get {
11+
(try? scalar("PRAGMA user_version") as? Int64).map(Int32.init)
12+
}
13+
set {
14+
_ = try? run("PRAGMA user_version = \(newValue ?? 0)")
15+
}
16+
}
17+
18+
/// The version of SQLite.
19+
/// See SQLite [sqlite_version()](https://sqlite.org/lang_corefunc.html#sqlite_version)
20+
var sqliteVersion: SQLiteVersion {
21+
guard let version = (try? scalar("SELECT sqlite_version()")) as? String,
22+
let splits = .some(version.split(separator: ".", maxSplits: 3)), splits.count == 3,
23+
let major = Int(splits[0]), let minor = Int(splits[1]), let point = Int(splits[2]) else {
24+
return (0, 0, 0)
25+
}
26+
return (major, minor, point)
27+
}
28+
29+
// Changing the foreign_keys setting affects the execution of all statements prepared using the database
30+
// connection, including those prepared before the setting was changed.
31+
//
32+
// https://sqlite.org/pragma.html#pragma_foreign_keys
33+
var foreignKeys: Bool {
34+
get { getBoolPragma("foreign_keys") }
35+
set { setBoolPragma("foreign_keys", newValue) }
36+
}
37+
38+
var deferForeignKeys: Bool {
39+
get { getBoolPragma("defer_foreign_keys") }
40+
set { setBoolPragma("defer_foreign_keys", newValue) }
41+
}
42+
43+
private func getBoolPragma(_ key: String) -> Bool {
44+
guard let binding = try? scalar("PRAGMA \(key)"),
45+
let intBinding = binding as? Int64 else { return false }
46+
return intBinding == 1
47+
}
48+
49+
private func setBoolPragma(_ key: String, _ newValue: Bool) {
50+
_ = try? run("PRAGMA \(key) = \(newValue ? "1" : "0")")
51+
}
52+
}

Sources/SQLite/Core/Connection.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -156,17 +156,6 @@ public final class Connection {
156156
Int(sqlite3_total_changes(handle))
157157
}
158158

159-
/// The user version of the database.
160-
/// See SQLite [PRAGMA user_version](https://sqlite.org/pragma.html#pragma_user_version)
161-
public var userVersion: Int32? {
162-
get {
163-
(try? scalar("PRAGMA user_version") as? Int64).map(Int32.init)
164-
}
165-
set {
166-
_ = try? run("PRAGMA user_version = \(newValue ?? 0)")
167-
}
168-
}
169-
170159
// MARK: - Execute
171160

172161
/// Executes a batch of SQL statements.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import Foundation
2+
3+
extension Connection {
4+
// https://sqlite.org/pragma.html#pragma_table_info
5+
//
6+
// This pragma returns one row for each column in the named table. Columns in the result set include the
7+
// column name, data type, whether or not the column can be NULL, and the default value for the column. The
8+
// "pk" column in the result set is zero for columns that are not part of the primary key, and is the
9+
// index of the column in the primary key for columns that are part of the primary key.
10+
func columnInfo(table: String) throws -> [ColumnDefinition] {
11+
func parsePrimaryKey(column: String) throws -> ColumnDefinition.PrimaryKey? {
12+
try createTableSQL(name: table).flatMap { .init(sql: $0) }
13+
}
14+
15+
let foreignKeys: [String: [ColumnDefinition.ForeignKey]] =
16+
Dictionary(grouping: try foreignKeyInfo(table: table), by: { $0.column })
17+
18+
return try run("PRAGMA table_info(\(table.quote()))").compactMap { row -> ColumnDefinition? in
19+
guard let name = row[1] as? String,
20+
let type = row[2] as? String,
21+
let notNull = row[3] as? Int64,
22+
let defaultValue = row[4] as? String?,
23+
let primaryKey = row[5] as? Int64 else { return nil }
24+
return ColumnDefinition(name: name,
25+
primaryKey: primaryKey == 1 ? try parsePrimaryKey(column: name) : nil,
26+
type: ColumnDefinition.Affinity.from(type),
27+
null: notNull == 0,
28+
defaultValue: .from(defaultValue),
29+
references: foreignKeys[name]?.first)
30+
}
31+
}
32+
33+
func indexInfo(table: String) throws -> [IndexDefinition] {
34+
func indexSQL(name: String) throws -> String? {
35+
try run("""
36+
SELECT sql FROM sqlite_master WHERE name=? AND type='index'
37+
UNION ALL
38+
SELECT sql FROM sqlite_temp_master WHERE name=? AND type='index'
39+
""", name, name)
40+
.compactMap { row in row[0] as? String }
41+
.first
42+
}
43+
44+
func columns(name: String) throws -> [String] {
45+
try run("PRAGMA index_info(\(name.quote()))").compactMap { row in
46+
row[2] as? String
47+
}
48+
}
49+
50+
return try run("PRAGMA index_list(\(table.quote()))").compactMap { row -> IndexDefinition? in
51+
guard let name = row[1] as? String,
52+
let unique = row[2] as? Int64,
53+
// Indexes SQLite creates implicitly for internal use start with "sqlite_".
54+
// See https://www.sqlite.org/fileformat2.html#intschema
55+
!name.starts(with: "sqlite_") else {
56+
return nil
57+
}
58+
return .init(table: table,
59+
name: name,
60+
unique: unique == 1,
61+
columns: try columns(name: name),
62+
indexSQL: try indexSQL(name: name))
63+
}
64+
}
65+
66+
func foreignKeyInfo(table: String) throws -> [ColumnDefinition.ForeignKey] {
67+
try run("PRAGMA foreign_key_list(\(table.quote()))").compactMap { row in
68+
if let table = row[2] as? String, // table
69+
let column = row[3] as? String, // from
70+
let primaryKey = row[4] as? String, // to
71+
let onUpdate = row[5] as? String,
72+
let onDelete = row[6] as? String {
73+
return .init(table: table, column: column, primaryKey: primaryKey,
74+
onUpdate: onUpdate == TableBuilder.Dependency.noAction.rawValue ? nil : onUpdate,
75+
onDelete: onDelete == TableBuilder.Dependency.noAction.rawValue ? nil : onDelete
76+
)
77+
} else {
78+
return nil
79+
}
80+
}
81+
}
82+
83+
// https://sqlite.org/pragma.html#pragma_foreign_key_check
84+
85+
// There are four columns in each result row.
86+
// The first column is the name of the table that
87+
// contains the REFERENCES clause.
88+
// The second column is the rowid of the row that contains the
89+
// invalid REFERENCES clause, or NULL if the child table is a WITHOUT ROWID table.
90+
// The third column is the name of the table that is referred to.
91+
// The fourth column is the index of the specific foreign key constraint that failed.
92+
func foreignKeyCheck() throws -> [ForeignKeyError] {
93+
try run("PRAGMA foreign_key_check").compactMap { row -> ForeignKeyError? in
94+
guard let table = row[0] as? String,
95+
let rowId = row[1] as? Int64,
96+
let target = row[2] as? String else { return nil }
97+
98+
return ForeignKeyError(from: table, rowId: rowId, to: target)
99+
}
100+
}
101+
102+
private func createTableSQL(name: String) throws -> String? {
103+
try run("""
104+
SELECT sql FROM sqlite_master WHERE name=? AND type='table'
105+
UNION ALL
106+
SELECT sql FROM sqlite_temp_master WHERE name=? AND type='table'
107+
""", name, name)
108+
.compactMap { row in row[0] as? String }
109+
.first
110+
}
111+
}

0 commit comments

Comments
 (0)