Skip to content

Commit 9dc478c

Browse files
committed
SchemaReader: parse and create unique constraints
1 parent cdaade1 commit 9dc478c

File tree

4 files changed

+98
-22
lines changed

4 files changed

+98
-22
lines changed

Sources/SQLite/Schema/SchemaDefinitions.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,19 +128,22 @@ public struct ColumnDefinition: Equatable {
128128
public let primaryKey: PrimaryKey?
129129
public let type: Affinity
130130
public let nullable: Bool
131+
public let unique: Bool
131132
public let defaultValue: LiteralValue
132133
public let references: ForeignKey?
133134

134135
public init(name: String,
135136
primaryKey: PrimaryKey? = nil,
136137
type: Affinity,
137138
nullable: Bool = true,
139+
unique: Bool = false,
138140
defaultValue: LiteralValue = .NULL,
139141
references: ForeignKey? = nil) {
140142
self.name = name
141143
self.primaryKey = primaryKey
142144
self.type = type
143145
self.nullable = nullable
146+
self.unique = unique
144147
self.defaultValue = defaultValue
145148
self.references = references
146149
}
@@ -244,16 +247,18 @@ public struct IndexDefinition: Equatable {
244247

245248
public enum Order: String { case ASC, DESC }
246249

247-
public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil, orders: [String: Order]? = nil) {
250+
public init(table: String, name: String, unique: Bool = false, columns: [String], `where`: String? = nil,
251+
orders: [String: Order]? = nil, origin: Origin? = nil) {
248252
self.table = table
249253
self.name = name
250254
self.unique = unique
251255
self.columns = columns
252256
self.where = `where`
253257
self.orders = orders
258+
self.origin = origin
254259
}
255260

256-
init (table: String, name: String, unique: Bool, columns: [String], indexSQL: String?) {
261+
init (table: String, name: String, unique: Bool, columns: [String], indexSQL: String?, origin: Origin? = nil) {
257262
func wherePart(sql: String) -> String? {
258263
IndexDefinition.whereRe.firstMatch(in: sql, options: [], range: NSRange(location: 0, length: sql.count)).map {
259264
(sql as NSString).substring(with: $0.range(at: 1))
@@ -278,7 +283,8 @@ public struct IndexDefinition: Equatable {
278283
unique: unique,
279284
columns: columns,
280285
where: indexSQL.flatMap(wherePart),
281-
orders: (orders?.isEmpty ?? false) ? nil : orders)
286+
orders: (orders?.isEmpty ?? false) ? nil : orders,
287+
origin: origin)
282288
}
283289

284290
public let table: String
@@ -287,6 +293,13 @@ public struct IndexDefinition: Equatable {
287293
public let columns: [String]
288294
public let `where`: String?
289295
public let orders: [String: Order]?
296+
public let origin: Origin?
297+
298+
public enum Origin: String {
299+
case uniqueConstraint = "u" // index created from a "CREATE TABLE (... UNIQUE)" column constraint
300+
case createIndex = "c" // index created explicitly via "CREATE INDEX ..."
301+
case primaryKey = "pk" // index created from a "CREATE TABLE PRIMARY KEY" column constraint
302+
}
290303

291304
enum IndexError: LocalizedError {
292305
case tooLong(String, String)
@@ -300,6 +313,13 @@ public struct IndexDefinition: Equatable {
300313
}
301314
}
302315

316+
// Indices with names of the form "sqlite_autoindex_TABLE_N" that are used to implement UNIQUE and PRIMARY KEY
317+
// constraints on ordinary tables.
318+
// https://sqlite.org/fileformat2.html#intschema
319+
var isInternal: Bool {
320+
name.starts(with: "sqlite_autoindex_")
321+
}
322+
303323
func validate() throws {
304324
if name.count > IndexDefinition.maxIndexLength {
305325
throw IndexError.tooLong(name, table)
@@ -348,6 +368,7 @@ extension ColumnDefinition {
348368
defaultValue.map { "DEFAULT \($0)" },
349369
primaryKey.map { $0.toSQL() },
350370
nullable ? nil : "NOT NULL",
371+
unique ? "UNIQUE" : nil,
351372
references.map { $0.toSQL() }
352373
].compactMap { $0 }
353374
.joined(separator: " ")

Sources/SQLite/Schema/SchemaReader.swift

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,35 @@ public class SchemaReader {
2121
let foreignKeys: [String: [ColumnDefinition.ForeignKey]] =
2222
Dictionary(grouping: try foreignKeys(table: table), by: { $0.column })
2323

24-
return try connection.prepareRowIterator("PRAGMA table_info(\(table.quote()))")
24+
let columnDefinitions = try connection.prepareRowIterator("PRAGMA table_info(\(table.quote()))")
2525
.map { (row: Row) -> ColumnDefinition in
2626
ColumnDefinition(
2727
name: row[TableInfoTable.nameColumn],
2828
primaryKey: (row[TableInfoTable.primaryKeyColumn] ?? 0) > 0 ?
2929
try parsePrimaryKey(column: row[TableInfoTable.nameColumn]) : nil,
3030
type: ColumnDefinition.Affinity(row[TableInfoTable.typeColumn]),
3131
nullable: row[TableInfoTable.notNullColumn] == 0,
32+
unique: false,
3233
defaultValue: LiteralValue(row[TableInfoTable.defaultValueColumn]),
3334
references: foreignKeys[row[TableInfoTable.nameColumn]]?.first
3435
)
3536
}
37+
38+
let internalIndexes = try indexDefinitions(table: table).filter { $0.isInternal }
39+
return columnDefinitions.map { definition in
40+
if let index = internalIndexes.first(where: { $0.columns.contains(definition.name) }), index.origin == .uniqueConstraint {
41+
42+
ColumnDefinition(name: definition.name,
43+
primaryKey: definition.primaryKey,
44+
type: definition.type,
45+
nullable: definition.nullable,
46+
unique: true,
47+
defaultValue: definition.defaultValue,
48+
references: definition.references)
49+
} else {
50+
definition
51+
}
52+
}
3653
}
3754

3855
public func objectDefinitions(name: String? = nil,
@@ -66,27 +83,26 @@ public class SchemaReader {
6683
.first
6784
}
6885

69-
func columns(name: String) throws -> [String] {
86+
func indexInfos(name: String) throws -> [IndexInfo] {
7087
try connection.prepareRowIterator("PRAGMA index_info(\(name.quote()))")
7188
.compactMap { row in
72-
row[IndexInfoTable.nameColumn]
89+
IndexInfo(name: row[IndexInfoTable.nameColumn],
90+
columnRank: row[IndexInfoTable.seqnoColumn],
91+
columnRankWithinTable: row[IndexInfoTable.cidColumn])
92+
7393
}
7494
}
7595

7696
return try connection.prepareRowIterator("PRAGMA index_list(\(table.quote()))")
7797
.compactMap { row -> IndexDefinition? in
7898
let name = row[IndexListTable.nameColumn]
79-
guard !name.starts(with: "sqlite_") else {
80-
// Indexes SQLite creates implicitly for internal use start with "sqlite_".
81-
// See https://www.sqlite.org/fileformat2.html#intschema
82-
return nil
83-
}
8499
return IndexDefinition(
85100
table: table,
86101
name: name,
87102
unique: row[IndexListTable.uniqueColumn] == 1,
88-
columns: try columns(name: name),
89-
indexSQL: try indexSQL(name: name)
103+
columns: try indexInfos(name: name).compactMap { $0.name },
104+
indexSQL: try indexSQL(name: name),
105+
origin: IndexDefinition.Origin(rawValue: row[IndexListTable.originColumn])
90106
)
91107
}
92108
}
@@ -123,6 +139,15 @@ public class SchemaReader {
123139
objectDefinitions(name: name, type: .table, temp: true)
124140
).compactMap(\.sql).first
125141
}
142+
143+
struct IndexInfo {
144+
let name: String?
145+
// The rank of the column within the index. (0 means left-most.)
146+
let columnRank: Int
147+
// The rank of the column within the table being indexed.
148+
// A value of -1 means rowid and a value of -2 means that an expression is being used
149+
let columnRankWithinTable: Int
150+
}
126151
}
127152

128153
private enum SchemaTable {
@@ -159,11 +184,12 @@ private enum TableInfoTable {
159184

160185
private enum IndexInfoTable {
161186
// The rank of the column within the index. (0 means left-most.)
162-
static let seqnoColumn = Expression<Int64>("seqno")
187+
static let seqnoColumn = Expression<Int>("seqno")
163188
// The rank of the column within the table being indexed.
164189
// A value of -1 means rowid and a value of -2 means that an expression is being used.
165-
static let cidColumn = Expression<Int64>("cid")
166-
// The name of the column being indexed. This columns is NULL if the column is the rowid or an expression.
190+
static let cidColumn = Expression<Int>("cid")
191+
// The name of the column being indexed.
192+
// This columns is NULL if the column is the rowid or an expression.
167193
static let nameColumn = Expression<String?>("name")
168194
}
169195

Tests/SQLiteTests/Schema/SchemaChangerTests.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ class SchemaChangerTests: SQLiteTestCase {
158158
func test_create_table() throws {
159159
try schemaChanger.create(table: "foo") { table in
160160
table.add(column: .init(name: "id", primaryKey: .init(autoIncrement: true), type: .INTEGER))
161-
table.add(column: .init(name: "name", type: .TEXT, nullable: false))
161+
table.add(column: .init(name: "name", type: .TEXT, nullable: false, unique: true))
162162
table.add(column: .init(name: "age", type: .INTEGER))
163163

164164
table.add(index: .init(table: table.name,
@@ -179,25 +179,28 @@ class SchemaChangerTests: SQLiteTestCase {
179179
primaryKey: .init(autoIncrement: true, onConflict: nil),
180180
type: .INTEGER,
181181
nullable: true,
182+
unique: false,
182183
defaultValue: .NULL,
183184
references: nil),
184185
ColumnDefinition(name: "name",
185186
primaryKey: nil,
186187
type: .TEXT,
187188
nullable: false,
189+
unique: true,
188190
defaultValue: .NULL,
189191
references: nil),
190192
ColumnDefinition(name: "age",
191193
primaryKey: nil,
192194
type: .INTEGER,
193195
nullable: true,
196+
unique: false,
194197
defaultValue: .NULL,
195198
references: nil)
196199
])
197200

198-
let indexes = try schema.indexDefinitions(table: "foo")
201+
let indexes = try schema.indexDefinitions(table: "foo").filter { !$0.isInternal }
199202
XCTAssertEqual(indexes, [
200-
IndexDefinition(table: "foo", name: "nameIndex", unique: true, columns: ["name"], where: nil, orders: nil)
203+
IndexDefinition(table: "foo", name: "nameIndex", unique: true, columns: ["name"], where: nil, orders: nil, origin: .createIndex)
201204
])
202205
}
203206

Tests/SQLiteTests/Schema/SchemaReaderTests.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,41 +18,48 @@ class SchemaReaderTests: SQLiteTestCase {
1818
primaryKey: .init(autoIncrement: false, onConflict: nil),
1919
type: .INTEGER,
2020
nullable: true,
21+
unique: false,
2122
defaultValue: .NULL,
2223
references: nil),
2324
ColumnDefinition(name: "email",
2425
primaryKey: nil,
2526
type: .TEXT,
2627
nullable: false,
28+
unique: true,
2729
defaultValue: .NULL,
2830
references: nil),
2931
ColumnDefinition(name: "age",
3032
primaryKey: nil,
3133
type: .INTEGER,
3234
nullable: true,
35+
unique: false,
3336
defaultValue: .NULL,
3437
references: nil),
3538
ColumnDefinition(name: "salary",
3639
primaryKey: nil,
3740
type: .REAL,
3841
nullable: true,
42+
unique: false,
3943
defaultValue: .NULL,
4044
references: nil),
4145
ColumnDefinition(name: "admin",
4246
primaryKey: nil,
4347
type: .NUMERIC,
4448
nullable: false,
49+
unique: false,
4550
defaultValue: .numericLiteral("0"),
4651
references: nil),
4752
ColumnDefinition(name: "manager_id",
4853
primaryKey: nil, type: .INTEGER,
4954
nullable: true,
55+
unique: false,
5056
defaultValue: .NULL,
5157
references: .init(table: "users", column: "manager_id", primaryKey: "id", onUpdate: nil, onDelete: nil)),
5258
ColumnDefinition(name: "created_at",
5359
primaryKey: nil,
5460
type: .NUMERIC,
5561
nullable: true,
62+
unique: false,
5663
defaultValue: .NULL,
5764
references: nil)
5865
])
@@ -68,6 +75,24 @@ class SchemaReaderTests: SQLiteTestCase {
6875
primaryKey: .init(autoIncrement: true, onConflict: .IGNORE),
6976
type: .INTEGER,
7077
nullable: true,
78+
unique: false,
79+
defaultValue: .NULL,
80+
references: nil)
81+
]
82+
)
83+
}
84+
85+
func test_columnDefinitions_parses_unique() throws {
86+
try db.run("CREATE TABLE t (name TEXT UNIQUE)")
87+
88+
let columns = try schemaReader.columnDefinitions(table: "t")
89+
XCTAssertEqual(columns, [
90+
ColumnDefinition(
91+
name: "name",
92+
primaryKey: nil,
93+
type: .TEXT,
94+
nullable: true,
95+
unique: true,
7196
defaultValue: .NULL,
7297
references: nil)
7398
]
@@ -128,13 +153,13 @@ class SchemaReaderTests: SQLiteTestCase {
128153
}
129154

130155
func test_indexDefinitions_no_index() throws {
131-
let indexes = try schemaReader.indexDefinitions(table: "users")
156+
let indexes = try schemaReader.indexDefinitions(table: "users").filter { !$0.isInternal }
132157
XCTAssertTrue(indexes.isEmpty)
133158
}
134159

135160
func test_indexDefinitions_with_index() throws {
136161
try db.run("CREATE UNIQUE INDEX index_users ON users (age DESC) WHERE age IS NOT NULL")
137-
let indexes = try schemaReader.indexDefinitions(table: "users")
162+
let indexes = try schemaReader.indexDefinitions(table: "users").filter { !$0.isInternal }
138163

139164
XCTAssertEqual(indexes, [
140165
IndexDefinition(
@@ -143,7 +168,8 @@ class SchemaReaderTests: SQLiteTestCase {
143168
unique: true,
144169
columns: ["age"],
145170
where: "age IS NOT NULL",
146-
orders: ["age": .DESC]
171+
orders: ["age": .DESC],
172+
origin: .createIndex
147173
)
148174
])
149175
}

0 commit comments

Comments
 (0)