diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 069a06d8fb..9e4f65cc1a 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -582,17 +582,31 @@ extension PrefetchedRowsDecoder: UnkeyedDecodingContainer { mutating func decode(_ type: T.Type) throws -> T where T: Decodable { defer { currentIndex += 1 } - let columnDecodingStrategy: DatabaseColumnDecodingStrategy + let row = rows[currentIndex] if let type = T.self as? any FetchableRecord.Type { - columnDecodingStrategy = type.databaseColumnDecodingStrategy - } else { - columnDecodingStrategy = .useDefaultKeys + // Prefer FetchableRecord decoding over Decodable. + return try type.init(row: row) as! T + } + + // Prefer DatabaseValueConvertible decoding over Decodable. + let columnDecoder = ColumnDecoder( + row: row, + columnIndex: 0, + codingPath: codingPath) + if type == Data.self { + return try columnDecoder.decode(type) + } else if type == Date.self { + return try columnDecoder.decode(type) + } else if T.self is any (DatabaseValueConvertible & StatementColumnConvertible).Type { + return try columnDecoder.decode(type) + } else if T.self is any DatabaseValueConvertible.Type { + return try columnDecoder.decode(type) } let decoder = _RowDecoder( - row: rows[currentIndex], + row: row, codingPath: codingPath, - columnDecodingStrategy: columnDecodingStrategy) + columnDecodingStrategy: .useDefaultKeys) return try T(from: decoder) } diff --git a/Tests/GRDBTests/QueryInterface/Association/AssociationPrefetchingCodableRecordTests.swift b/Tests/GRDBTests/QueryInterface/Association/AssociationPrefetchingCodableRecordTests.swift index 7877a5e2dd..92a6d2c048 100644 --- a/Tests/GRDBTests/QueryInterface/Association/AssociationPrefetchingCodableRecordTests.swift +++ b/Tests/GRDBTests/QueryInterface/Association/AssociationPrefetchingCodableRecordTests.swift @@ -1526,4 +1526,92 @@ class AssociationPrefetchingCodableRecordTests: GRDBTestCase { } } } + + func testIncludingAllHasMany_PrefersDatabaseValueConvertibleOverDecodable() throws { + struct Value: Decodable, DatabaseValueConvertible, Equatable { + var string: String + + init(string: String) { + self.string = string + } + + init(from decoder: Decoder) throws { + string = try decoder.singleValueContainer().decode(String.self) + " (Decodable)" + } + + var databaseValue: DatabaseValue { fatalError("irrelevant") } + + static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { + String.fromDatabaseValue(databaseValue) + .map { Value(string: $0 + " (DatabaseValueConvertible)") } + } + } + + struct Record: FetchableRecord, Decodable { + var a: A + var values: [Value] + } + + let request = A + .including(all: A + .hasMany(B.self, key: "values") + .select(Column("colb3")) + .orderByPrimaryKey()) + .orderByPrimaryKey() + + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let records = try Record.fetchAll(db, request) + XCTAssertEqual(records.map(\.values), [ + [ + Value(string: "b1 (DatabaseValueConvertible)"), + Value(string: "b2 (DatabaseValueConvertible)"), + ], + [Value(string: "b3 (DatabaseValueConvertible)")], + [], + ]) + } + } + + func testIncludingAllHasMany_PrefersFetchableRecordOverDecodable() throws { + struct Value: FetchableRecord, Decodable, Equatable { + var string: String + + init(string: String) { + self.string = string + } + + init(row: Row) throws { + string = row["colb3"] + " (FetchableRecord)" + } + + init(from _: Decoder) throws { + string = "Decodable" + } + } + + struct Record: FetchableRecord, Decodable { + var a: A + var values: [Value] + } + + let request = A + .including(all: A + .hasMany(B.self, key: "values") + .orderByPrimaryKey()) + .orderByPrimaryKey() + + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let records = try Record.fetchAll(db, request) + XCTAssertEqual(records.map(\.values), [ + [ + Value(string: "b1 (FetchableRecord)"), + Value(string: "b2 (FetchableRecord)"), + ], + [Value(string: "b3 (FetchableRecord)")], + [], + ]) + } + } }