Skip to content
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

Fix a bug in Decodable support #1533

Merged
merged 3 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions GRDB/Record/FetchableRecord+Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ private struct _RowDecoder<R: FetchableRecord>: Decoder {

lazy var allKeys: [Key] = {
let row = decoder.row
// TODO: test when _columnForKey is not nil
var keys = _columnForKey.map { Set($0.keys) } ?? Set(row.columnNames)
keys.formUnion(row.scopesTree.names)
keys.formUnion(row.prefetchedRows.keys)
Expand Down Expand Up @@ -174,15 +175,25 @@ private struct _RowDecoder<R: FetchableRecord>: Decoder {

func decodeNil(forKey key: Key) throws -> Bool {
let row = decoder.row
if let column = try? decodeColumn(forKey: key), row[column] != nil {
return false

// Column?
if let column = try? decodeColumn(forKey: key),
let index = row.index(forColumn: column)
{
return row.hasNull(atIndex: index)
}
if row.scopesTree[key.stringValue] != nil {
return false

// Scope?
if let scopedRow = row.scopesTree[key.stringValue] {
return scopedRow.containsNonNullValue == false
}
if row.prefetchedRows[key.stringValue] != nil {

// Prefetched Rows?
if let prefetchedRows = row.prefetchedRows[key.stringValue] {
return false
}

// Unknown key
return true
}

Expand Down Expand Up @@ -376,7 +387,35 @@ private struct _RowDecoder<R: FetchableRecord>: Decoder {
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key)
throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey
{
fatalError("not implemented")
let row = decoder.row

// Column?
if let column = try? decodeColumn(forKey: key),
let index = row.index(forColumn: column)
{
// We need a JSON container, but how do we create one?
throw DecodingError.typeMismatch(
KeyedDecodingContainer<NestedKey>.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "not implemented: building a nested JSON container for the column '\(column)'"))
}

// Scope?
if let scopedRow = row.scopesTree[key.stringValue] {
return KeyedDecodingContainer(KeyedContainer<NestedKey>(decoder: _RowDecoder(
row: scopedRow,
codingPath: codingPath + [key],
columnDecodingStrategy: decoder.columnDecodingStrategy)))
}

// Don't look for prefetched rows: those need a unkeyed container.

throw DecodingError.typeMismatch(
KeyedDecodingContainer<NestedKey>.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No keyed container found for key '\(key)'"))
}

func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
Expand Down
262 changes: 262 additions & 0 deletions Tests/GRDBTests/FetchableRecordDecodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1652,3 +1652,265 @@ extension FetchableRecordDecodableTests {
}
}
}

// MARK: - KeyedContainer tests

extension FetchableRecordDecodableTests {
struct AnyCodingKey: CodingKey {
var stringValue: String
var intValue: Int? { nil }

init(_ key: String) {
self.stringValue = key
}

init(stringValue: String) {
self.stringValue = stringValue
}

init?(intValue: Int) {
return nil
}
}

func test_allKeys_and_containsKey() throws {
struct Witness: Decodable, FetchableRecord {
init(from decoder: any Decoder) throws {
// Top
let container = try decoder.container(keyedBy: AnyCodingKey.self)
do {
// Test allKeys
let allKeys = container.allKeys
XCTAssertEqual(Set(allKeys.map(\.stringValue)), [
"a",
"topLevelScope1",
"topLevelScope2",
"nestedScope1",
"nestedScope2",
"prefetchedRows1",
"prefetchedRows2"])

// Test contains(_:)
for key in allKeys {
XCTAssertTrue(container.contains(key))
}
XCTAssertFalse(container.contains(AnyCodingKey("b")))
XCTAssertFalse(container.contains(AnyCodingKey("c")))
}

// topLevelScope1
let topLevelScope1Container = try container.nestedContainer(
keyedBy: AnyCodingKey.self,
forKey: AnyCodingKey("topLevelScope1"))
do {
// Test allKeys
let allKeys = topLevelScope1Container.allKeys
XCTAssertEqual(Set(allKeys.map(\.stringValue)), [
"c",
])

// Test contains(_:)
for key in allKeys {
XCTAssertTrue(topLevelScope1Container.contains(key))
}
}

// topLevelScope2
let topLevelScope2Container = try container.nestedContainer(
keyedBy: AnyCodingKey.self,
forKey: AnyCodingKey("topLevelScope2"))
do {
// Test allKeys
let allKeys = topLevelScope2Container.allKeys
XCTAssertEqual(Set(allKeys.map(\.stringValue)), [
"nestedScope2",
"nestedScope1",
"prefetchedRows2",
])

// Test contains(_:)
for key in allKeys {
XCTAssertTrue(topLevelScope2Container.contains(key))
}
}
}
}

try makeDatabaseQueue().read { db in
let row = try Row.fetchOne(
db, sql: """
SELECT 1 AS a, -- main row
2 AS b, -- not exposed
3 AS c, -- scope topLevelScope1
4 AS d, -- scope topLevelScope2.nestedScope1
5 AS e -- scope topLevelScope2.nestedScope2
""",
adapter: RangeRowAdapter(0..<1)
.addingScopes([
"topLevelScope1": RangeRowAdapter(2..<3),
"topLevelScope2": EmptyRowAdapter().addingScopes([
"nestedScope1": RangeRowAdapter(3..<4),
"nestedScope2": RangeRowAdapter(4..<5),
]),
]))!

row.prefetchedRows.setRows([], forKeyPath: ["prefetchedRows1"])
row.prefetchedRows.setRows([Row()], forKeyPath: ["topLevelScope2", "prefetchedRows2"])
// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:1]
unadapted: [a:1 b:2 c:3 d:4 e:5]
- topLevelScope1: [c:3]
- topLevelScope2: []
- nestedScope1: [d:4]
- nestedScope2: [e:5]
+ prefetchedRows2: 1 row
+ prefetchedRows1: 0 row
+ prefetchedRows2: 1 row
""")

// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
}

// Regression test for <https://github.com/groue/GRDB.swift/issues/1531>
func test_decodeNil_and_containsKey() throws {
struct Witness: Decodable, FetchableRecord {
struct NestedRecord: Decodable, FetchableRecord { }

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)

// column
do {
let key = AnyCodingKey("a")
let nilDecoded = try container.decodeNil(forKey: key)
let value = try container.decodeIfPresent(Int.self, forKey: key)
XCTAssertTrue(nilDecoded == (value == nil))
XCTAssertTrue(container.contains(key))
}

// scope
do {
let key = AnyCodingKey("nested")
let nilDecoded = try container.decodeNil(forKey: key)
let value = try container.decodeIfPresent(NestedRecord.self, forKey: key)
XCTAssertTrue(nilDecoded == (value == nil))
XCTAssertTrue(container.contains(key))
}

// missing key
do {
let key = AnyCodingKey("missing")
try XCTAssertTrue(container.decodeNil(forKey: key))
try XCTAssertNil(container.decodeIfPresent(Int.self, forKey: key))
try XCTAssertNil(container.decodeIfPresent(NestedRecord.self, forKey: key))
XCTAssertFalse(container.contains(key))
}
}
}

try makeDatabaseQueue().read { db in
do {
let row = try Row.fetchOne(
db, sql: """
SELECT 1 AS a, 2 AS b
""",
adapter: ScopeAdapter([
"nested": RangeRowAdapter(1..<2),
]))!

// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:1 b:2]
unadapted: [a:1 b:2]
- nested: [b:2]
""")

// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}

do {
let row = try Row.fetchOne(
db, sql: """
SELECT NULL AS a, NULL AS b
""",
adapter: ScopeAdapter([
"nested": RangeRowAdapter(1..<2),
]))!

// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:NULL b:NULL]
unadapted: [a:NULL b:NULL]
- nested: [b:NULL]
""")

// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
}
}

// Regression test for <https://github.com/groue/GRDB.swift/issues/1531>
func test_decodeNil_when_scope_and_column_have_the_same_name() throws {
struct Witness: Decodable, FetchableRecord {
struct NestedRecord: Decodable, FetchableRecord { }

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)

let key = AnyCodingKey("a")
let nilDecoded = try container.decodeNil(forKey: key)
let intValue = try container.decodeIfPresent(Int.self, forKey: key)
let recordValue = try container.decodeIfPresent(NestedRecord.self, forKey: key)
XCTAssertTrue(nilDecoded == (intValue == nil))
XCTAssertTrue(nilDecoded == (recordValue == nil))
}
}

try makeDatabaseQueue().read { db in
do {
let row = try Row.fetchOne(
db, sql: """
SELECT 1 AS a
""",
adapter: ScopeAdapter([
"a": SuffixRowAdapter(fromIndex: 0),
]))!

// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:1]
unadapted: [a:1]
- a: [a:1]
""")

// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}

do {
let row = try Row.fetchOne(
db, sql: """
SELECT NULL AS a
""",
adapter: ScopeAdapter([
"a": SuffixRowAdapter(fromIndex: 0),
]))!

// Check test setup
XCTAssertEqual(row.debugDescription, """
▿ [a:NULL]
unadapted: [a:NULL]
- a: [a:NULL]
""")

// Test keyed container
_ = try FetchableRecordDecoder().decode(Witness.self, from: row)
}
}
}
}