Skip to content

Commit 595eecb

Browse files
authored
Improve query decoding errors (#77)
* Improve query decoding errors Let's surface more information about which column failed to decode. * wip * wip
1 parent f109bab commit 595eecb

6 files changed

Lines changed: 111 additions & 59 deletions

File tree

Package.resolved

Lines changed: 16 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

SharingGRDB.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/StructuredQueriesGRDBCore/QueryCursor.swift

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,29 @@ public class QueryCursor<Element>: DatabaseCursor {
2525
public func _element(sqliteStatement _: SQLiteStatement) throws -> Element {
2626
fatalError("Abstract method should be overridden in subclass")
2727
}
28+
29+
@usableFromInline
30+
struct DecodingError: Error, CustomStringConvertible {
31+
let columnIndex: Int
32+
let columnName: String
33+
let sql: String
34+
35+
@usableFromInline
36+
init(columnIndex: Int, columnName: String, sql: String) {
37+
self.columnIndex = columnIndex
38+
self.columnName = columnName
39+
self.sql = sql
40+
}
41+
42+
@usableFromInline
43+
var description: String {
44+
"""
45+
Expected column \(columnIndex) (\(columnName.debugDescription)) to not be NULL: …
46+
47+
\(sql)
48+
"""
49+
}
50+
}
2851
}
2952

3053
@usableFromInline
@@ -40,9 +63,18 @@ final class QueryValueCursor<QueryValue: QueryRepresentable>: QueryCursor<QueryV
4063

4164
@inlinable
4265
public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element {
43-
let element = try QueryValue(decoder: &decoder).queryOutput
44-
decoder.next()
45-
return element
66+
do {
67+
let element = try QueryValue(decoder: &decoder).queryOutput
68+
decoder.next()
69+
return element
70+
} catch QueryDecodingError.missingRequiredColumn {
71+
let columnIndex = Int(decoder.currentIndex) - 1
72+
throw DecodingError(
73+
columnIndex: columnIndex,
74+
columnName: _statement.columnNames[columnIndex],
75+
sql: _statement.sql
76+
)
77+
}
4678
}
4779
}
4880

@@ -62,9 +94,18 @@ final class QueryPackCursor<
6294

6395
@inlinable
6496
public override func _element(sqliteStatement _: SQLiteStatement) throws -> Element {
65-
let element = try decoder.decodeColumns((repeat each QueryValue).self)
66-
decoder.next()
67-
return element
97+
do {
98+
let element = try decoder.decodeColumns((repeat each QueryValue).self)
99+
decoder.next()
100+
return element
101+
} catch QueryDecodingError.missingRequiredColumn {
102+
let columnIndex = Int(decoder.currentIndex) - 1
103+
throw DecodingError(
104+
columnIndex: columnIndex,
105+
columnName: _statement.columnNames[columnIndex],
106+
sql: _statement.sql
107+
)
108+
}
68109
}
69110
}
70111

Tests/SharingGRDBTests/FetchAllTests.swift

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ struct FetchAllTests {
2121
for index in 1...count {
2222
group.addTask {
2323
try await database.write { db in
24-
try Record.insert(Record(id: index)).execute(db)
24+
try Record.insert { Record(id: index) }.execute(db)
2525
}
2626
}
2727
}
@@ -31,7 +31,7 @@ struct FetchAllTests {
3131
#expect(records == (1...count).map { Record(id: $0) })
3232

3333
await withThrowingTaskGroup { group in
34-
for index in 1...(count/2) {
34+
for index in 1...(count / 2) {
3535
group.addTask {
3636
try await database.write { db in
3737
try Record.find(index * 2).delete().execute(db)
@@ -41,7 +41,27 @@ struct FetchAllTests {
4141
}
4242

4343
try await $records.load()
44-
#expect(records == (0...(count/2-1)).map { Record(id: $0 * 2 + 1) })
44+
#expect(records == (0...(count / 2 - 1)).map { Record(id: $0 * 2 + 1) })
45+
}
46+
47+
@Test func fetchFailure() {
48+
do {
49+
try database.read { db in
50+
_ =
51+
try Record
52+
.select { ($0.id, $0.date, #sql("\($0.optionalDate)", as: Date.self)) }
53+
.fetchAll(db)
54+
}
55+
Issue.record()
56+
} catch {
57+
#expect(
58+
"\(error)".contains(
59+
"""
60+
Expected column 2 ("optionalDate") to not be NULL
61+
"""
62+
)
63+
)
64+
}
4565
}
4666
}
4767

@@ -60,15 +80,15 @@ extension DatabaseWriter where Self == DatabaseQueue {
6080
try #sql(
6181
"""
6282
CREATE TABLE "records" (
63-
"id" INTEGER PRIMARY KEY AUTOINCREMENT
64-
, "date" INTEGER NOT NULL DEFAULT 42
65-
, "optionalDate" INTEGER
83+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
84+
"date" INTEGER NOT NULL DEFAULT 42,
85+
"optionalDate" INTEGER
6686
)
6787
"""
6888
)
6989
.execute(db)
7090
for _ in 1...3 {
71-
_ = try Record.insert(Record.Draft()).execute(db)
91+
_ = try Record.insert { Record.Draft() }.execute(db)
7292
}
7393
}
7494
return database

Tests/SharingGRDBTests/FetchTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ extension DatabaseWriter where Self == DatabaseQueue {
5050
)
5151
.execute(db)
5252
for _ in 1...3 {
53-
_ = try Record.insert(Record.Draft()).execute(db)
53+
_ = try Record.insert { Record.Draft() }.execute(db)
5454
}
5555
}
5656
try migrator.migrate(database)

Tests/SharingGRDBTests/IntegrationTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ struct IntegrationTests {
1515
#expect(syncUps == [])
1616

1717
try await database.write { db in
18-
_ = try SyncUp.insert(SyncUp.Draft(isActive: true, title: "Engineering"))
18+
_ = try SyncUp.insert { SyncUp.Draft(isActive: true, title: "Engineering") }
1919
.execute(db)
2020
}
2121
try await $syncUps.load()
2222
#expect(syncUps == [SyncUp(id: 1, isActive: true, title: "Engineering")])
2323
try await database.write { db in
24-
_ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: false, title: "Engineering"))
24+
_ = try SyncUp.upsert { SyncUp.Draft(id: 1, isActive: false, title: "Engineering") }
2525
.execute(db)
2626
}
2727
try await $syncUps.load()
2828
#expect(syncUps == [])
2929
try await database.write { db in
30-
_ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: true, title: "Engineering"))
30+
_ = try SyncUp.upsert { SyncUp.Draft(id: 1, isActive: true, title: "Engineering") }
3131
.execute(db)
3232
}
3333
try await $syncUps.load()
@@ -40,19 +40,19 @@ struct IntegrationTests {
4040
#expect(syncUps == [])
4141

4242
try await database.write { db in
43-
_ = try SyncUp.insert(SyncUp.Draft(isActive: true, title: "Engineering"))
43+
_ = try SyncUp.insert { SyncUp.Draft(isActive: true, title: "Engineering") }
4444
.execute(db)
4545
}
4646
try await $syncUps.load()
4747
#expect(syncUps == [SyncUp(id: 1, isActive: true, title: "Engineering")])
4848
try await database.write { db in
49-
_ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: false, title: "Engineering"))
49+
_ = try SyncUp.upsert { SyncUp.Draft(id: 1, isActive: false, title: "Engineering") }
5050
.execute(db)
5151
}
5252
try await $syncUps.load()
5353
#expect(syncUps == [])
5454
try await database.write { db in
55-
_ = try SyncUp.upsert(SyncUp.Draft(id: 1, isActive: true, title: "Engineering"))
55+
_ = try SyncUp.upsert { SyncUp.Draft(id: 1, isActive: true, title: "Engineering") }
5656
.execute(db)
5757
}
5858
try await $syncUps.load()

0 commit comments

Comments
 (0)