-
-
Notifications
You must be signed in to change notification settings - Fork 711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unable to decode database NULL value as Swift nil value of type Int? when using ScopeAdapter built from splittingRowAdapters #1531
Comments
Thanks for the report, @mallman. Please hold on! |
Hi again, @mallman, Let's first understand why you're unlucky. You say So something is defining a scope. And it has the same name as your key. Does this ring a bell to you? I think there is a bug in the GRDB snippet you have mentioned, because scoped rows are usually used to decode other records, and those records are decoded as nil if the scoped row only contains NULL values. To illustrate, let's run this code: struct Team { }
struct Player {
static let team = belongsTo(Team.self) // defines the "team" association key
}
struct PlayerInfo: Decodable, FetchableRecord {
var player: Player
var team: Team? // decoded from the "team" scope
}
let infos = try Player
.including(optional: Player.team) // defines the "team" scope
.asRequest(of: PlayerInfo.self)
.fetchAll(db) The SQL request is: -- the "team" scope
-- <---->
SELECT player.*, team.*
FROM player
LEFT JOIN team ON team.id = player.teamId Let's pretend the raw rows are:
In all rows, But in the third row, the team row only contains NULL values. This means that YET Do you think I described your issue? |
I think you've hit the nail on the head. Let me give you a pared-down look at some of the record decoding in question: struct BrowserItemRecord: Decodable, FetchableRecord {
let stackSize: Int?
let version: VersionRecord
private static func adapter(_ db: Database) throws -> RowAdapter {
let adapters = try splittingRowAdapters(columnCounts: [
1, // stackSize
VersionRecord.numberOfSelectedColumns(db)
])
return ScopeAdapter([
CodingKeys.stackSize.stringValue: adapters[0],
CodingKeys.version.stringValue: adapters[1]
}
} I decode a From the way you've described it, it sounds like the issue for GRDB is it doesn't like it when a row scope has a single column? Does that sound right? Thank you very much! |
There's a bug in GRDB, that's for sure. But Look at But When I look at the adapter, which defines a
This is weird. The expected JSON is instead: {
"stackSize": 123,
"version": { }
} I'm thus not sure the adapter is well-fitted to I'll make some checks and come back to you. I don't have clear ideas about the expected behavior when a row contains BOTH a column named |
The JSON representation you present is indeed weird. Are you suggesting that for every value Let me investigate an alternative. Meanwhile, if you have any suggestions for a better way to define a row adapter (or otherwise decode a record like mine), please do. I could define a separate record to bring together all of the single column properties, and then define an adapter that includes that record. It's something to consider. |
No. Sorry I wasn't clear. It's more about nesting and grouping columns. Decoding an object named Actually, I'd suggest to remove the "stackSize" scope. The snippet below is identical to yours, except for the defined scopes: struct BrowserItemRecord: Decodable, FetchableRecord {
let stackSize: Int?
let version: VersionRecord
private static func adapter(_ db: Database) throws -> RowAdapter {
let adapters = try splittingRowAdapters(columnCounts: [
1, // stackSize
VersionRecord.numberOfSelectedColumns(db)
])
return ScopeAdapter([
// No scope for stackSize
CodingKeys.version.stringValue: adapters[1]
])
}
} |
If I come back to a toy example, and still using JSON as a comparison: Consider the following object: struct Membership {
var player: Player
var team: Team
} We expect the following JSON: {
"player": {
"id": 1,
"name": "Arthur"
},
"team": {
"id": 2,
"name": "Reds"
}
} And the JSON below would not work well, because it is flat and has ambiguous keys: {
"id": 1,
"name": "Arthur",
"id": 2,
"name": "Reds"
} But this flat JSON is exactly what SQL provides, without any further processing: SELECT player.*, team.* FROM ... Raw database rows are flat, and have ambiguous columns:
So we need to introduce nesting, and that's what scopes can do:
In your
This would match the expected JSON: {
"stackSize": 123,
"version": {
"major": 1,
"minor": 0,
"patch": 0
}
} Things could turn more complicated in |
When debugging, you may enjoy the let request = /* your request */
if let row = try Row.fetchOne(db, request) {
print(row.debugDescription)
} With two scopes (your initial code), I'd expect something which looks like below. See how
With only one scope (version), I'd expect something like:
From such a row, |
The #1533 PR fixes your issue, @mallman. Your app will happen to work as you expect, even if it misuses the But I really suggest that you remove the "stackSize" scope. Your private static func adapter(_ db: Database) throws -> RowAdapter {
// Return an adapter that defines the "version" scope,
// aimed at decoding the `version` record property.
// The "version" scope is made of all `VersionRecord` columns,
// starting after the first column (stackSize):
let adapters = try splittingRowAdapters(columnCounts: [
1, // stackSize
VersionRecord.numberOfSelectedColumns(db)
])
return ScopeAdapter([
CodingKeys.version.stringValue: adapters[1]
])
} If your database row is made of an initial "stackSize" columns, and all other columns feed private static func adapter(_ db: Database) throws -> RowAdapter {
// Return an adapter that defines the "version" scope,
// aimed at decoding the `version` record property.
// The "version" scope is all columns but the first (stackSize):
ScopeAdapter([
CodingKeys.version.stringValue: SuffixRowAdapter(fromIndex: 1)
])
} |
I comment much too much in this issue, @mallman, but let me share a debugging tip. Whenever you have issues decoding a struct MyRecord: Decodable, FetchableRecord {
#warning("TODO: remove this debugging initializer")
init(row: Row) throws {
print(row.debugDescription)
self = try FetchableRecordDecoder().decode(MyRecord.self, from: row)
}
} The debugging output can help understanding issues, or write bug reports. |
The fix has shipped in 6.27.0. |
Hi @groue. Thank you for your prompt action in fixing this bug and all of your helpful tips. I've taken your advice to heart and have eliminated "scopes" for single column "records". That has been working for me. I've also taken the time to revisit, reorganize and rewrite a lot of my GRDB hack code, improving its rigor and reducing complexity. For example, I have pulled up two subqueries in a query into a join and select on two CTEs instead, and I'm using GRDB's CTE support and Overall, I'ver performed a large amount of housekeeping motivated by this one issue. Cheers. |
Thank you @mallman :-) Be assured I do appreciate your contributions as well! |
What did you do?
I have a ridiculously complicated custom
Decodable, FetchableRecord
with several optional fields. I decode these values from a nasty horrible SQL query with multiple nested subqueries, joins, aggregations, windows, filters and sorts. The point is, please don't ask me to post my record type definition and SQL.Be that as it may, I think I have everything set up to decode a
Row
into my record correctly. However, GRDB is getting confused when I return aNULL
in a column that's supposed to be decoded as a SwiftInt?
. In fact, if I return some non-NULL
value then GRDB decodes the row successfully.In my attempt to debug the issue, I found that the
decodeNil()
method onFetchableRecord+Decodable
is returningfalse
even though the row column value isNULL
. I would expect it to returntrue
, as the protocol documentation for that method implies.Here is the decoder method I'm referring to:
When running my row/column through this method,
decodeColumn(forKey: key)
decodes to the correct column name, butrow[column] == nil
. So then we go torow.scopesTree[key.stringValue]
. I don't understand this.row.scopesTree[key.stringValue]
is notnil
for my coding key, sodecodeNil()
returnsfalse
. That is not the expected behavior.How can I help you help me fix this?
Thank you.
Environment
GRDB flavor(s): Custom SQLite 3.45.0
GRDB version: 6.25.0
Installation method: manual framework
Xcode version: 15.3
Swift version: 5.10
Platform(s) running GRDB: macOS
macOS version running Xcode: 14.4.1
The text was updated successfully, but these errors were encountered: