Skip to content

Commit 78cb0d6

Browse files
committed
Improve query decoding errors
Let's surface more information about which column failed to decode.
1 parent f109bab commit 78cb0d6

5 files changed

Lines changed: 90 additions & 32 deletions

File tree

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: 22 additions & 5 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
}
@@ -43,6 +43,23 @@ struct FetchAllTests {
4343
try await $records.load()
4444
#expect(records == (0...(count/2-1)).map { Record(id: $0 * 2 + 1) })
4545
}
46+
47+
@Test func fetchFailure() {
48+
do {
49+
try database.read { db in
50+
_ = try Record
51+
.select { ($0.id, $0.date, #sql("\($0.optionalDate)", as: Date.self)) }
52+
.fetchAll(db)
53+
}
54+
Issue.record()
55+
} catch {
56+
#expect("\(error)".contains(
57+
"""
58+
Expected column 2 ("optionalDate") to not be NULL
59+
"""
60+
)
61+
}
62+
}
4663
}
4764

4865
@Table
@@ -60,15 +77,15 @@ extension DatabaseWriter where Self == DatabaseQueue {
6077
try #sql(
6178
"""
6279
CREATE TABLE "records" (
63-
"id" INTEGER PRIMARY KEY AUTOINCREMENT
64-
, "date" INTEGER NOT NULL DEFAULT 42
65-
, "optionalDate" INTEGER
80+
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
81+
"date" INTEGER NOT NULL DEFAULT 42,
82+
"optionalDate" INTEGER
6683
)
6784
"""
6885
)
6986
.execute(db)
7087
for _ in 1...3 {
71-
_ = try Record.insert(Record.Draft()).execute(db)
88+
_ = try Record.insert { Record.Draft() }.execute(db)
7289
}
7390
}
7491
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)