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

Support for single value decoding #1574

Merged
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
54 changes: 51 additions & 3 deletions GRDB/Record/FetchableRecord+Decodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,12 @@ private struct _RowDecoder<R: FetchableRecord>: Decoder {

func singleValueContainer() throws -> SingleValueDecodingContainer {
guard let key = codingPath.last else {
// Decoding an array of scalars from rows: pick the first column
return ColumnDecoder<R>(row: row, columnIndex: 0, codingPath: codingPath)
// Not yet sure what we are decoding, this will be decided in the SingleValueDecodingContainer functions.
// For decoding an array of scalars (in case of prefetched rows) we pick the first column.
return SingleValueRowDecoder(
columnDecoder: ColumnDecoder<R>(row: row, columnIndex: 0, codingPath: codingPath),
columnDecodingStrategy: columnDecodingStrategy
)
}
guard let index = row.index(forColumn: key.stringValue) else {
// Don't use DecodingError.keyNotFound:
Expand Down Expand Up @@ -346,7 +350,7 @@ private struct _RowDecoder<R: FetchableRecord>: Decoder {

// Unknown key
//
// Should be throw an error? Well... The use case is the following:
// Should we throw an error? Well... The use case is the following:
//
// // SELECT book.*, author.* FROM book
// // JOIN author ON author.id = book.authorId
Expand Down Expand Up @@ -487,6 +491,50 @@ private struct _RowDecoder<R: FetchableRecord>: Decoder {
}
}

private struct SingleValueRowDecoder<R: FetchableRecord>: SingleValueDecodingContainer {
var columnDecoder: ColumnDecoder<R>
var columnDecodingStrategy: DatabaseColumnDecodingStrategy
let codingPath: [any CodingKey] = []

func decodeNil() -> Bool { columnDecoder.decodeNil() }
func decode(_ type: Bool.Type) throws -> Bool { try columnDecoder.decode(type) }
func decode(_ type: String.Type) throws -> String { try columnDecoder.decode(type) }
func decode(_ type: Double.Type) throws -> Double { try columnDecoder.decode(type) }
func decode(_ type: Float.Type) throws -> Float { try columnDecoder.decode(type) }
func decode(_ type: Int.Type) throws -> Int { try columnDecoder.decode(type) }
func decode(_ type: Int8.Type) throws -> Int8 { try columnDecoder.decode(type) }
func decode(_ type: Int16.Type) throws -> Int16 { try columnDecoder.decode(type) }
func decode(_ type: Int32.Type) throws -> Int32 { try columnDecoder.decode(type) }
func decode(_ type: Int64.Type) throws -> Int64 { try columnDecoder.decode(type) }
#if compiler(>=6)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
func decode(_ type: Int128.Type) throws -> Int128 { try columnDecoder.decode(type) }
#endif
func decode(_ type: UInt.Type) throws -> UInt { try columnDecoder.decode(type) }
func decode(_ type: UInt8.Type) throws -> UInt8 { try columnDecoder.decode(type) }
func decode(_ type: UInt16.Type) throws -> UInt16 { try columnDecoder.decode(type) }
func decode(_ type: UInt32.Type) throws -> UInt32 { try columnDecoder.decode(type) }
func decode(_ type: UInt64.Type) throws -> UInt64 { try columnDecoder.decode(type) }
#if compiler(>=6)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
func decode(_ type: UInt128.Type) throws -> UInt128 { try columnDecoder.decode(type) }
#endif

func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
if let type = T.self as? any FetchableRecord.Type {
// Prefer FetchableRecord decoding over Decodable.
return try type.init(row: columnDecoder.row) as! T
} else {
let decoder = _RowDecoder<R>(
row: columnDecoder.row,
codingPath: [],
columnDecodingStrategy: columnDecodingStrategy
)
return try T(from: decoder)
}
}
}

// MARK: - PrefetchedRowsDecoder

private struct PrefetchedRowsDecoder<R: FetchableRecord>: Decoder {
Expand Down
66 changes: 66 additions & 0 deletions Tests/GRDBTests/FetchableRecordDecodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1913,4 +1913,70 @@ extension FetchableRecordDecodableTests {
}
}
}

// Regression test for <https://github.com/groue/GRDB.swift/issues/1572>
func testSingleValueContainer() throws {
struct Struct: Decodable {
let value: String
}

struct Wrapper<Model: Decodable>: FetchableRecord, Decodable {
var model: Model
var otherValue: String

enum CodingKeys: String, CodingKey {
case otherValue
}

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherValue = try container.decode(String.self, forKey: . otherValue)

let singleValueContainer = try decoder.singleValueContainer()
model = try singleValueContainer.decode(Model.self)
}
}

let row = Row(["value": "foo", "otherValue": "bar"])

let wrapper = try Wrapper<Struct>(row: row)
XCTAssertEqual(wrapper.model.value, "foo")
XCTAssertEqual(wrapper.otherValue, "bar")
}

// Regression test for <https://github.com/groue/GRDB.swift/issues/1572>
// Here we test that `FetchableRecord` takes precedence over `Decodable`
// when a record is encoded with a `SingleValueEncodingContainer`.
func testSingleValueContainerWithFetchableRecord() throws {
struct Struct: Decodable, FetchableRecord {
let value: String

init(row: Row) throws {
value = row["actualValue"]
}
}

struct Wrapper<Model: Decodable>: FetchableRecord, Decodable {
var model: Model
var otherValue: String

enum CodingKeys: String, CodingKey {
case otherValue
}

init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherValue = try container.decode(String.self, forKey: . otherValue)

let singleValueContainer = try decoder.singleValueContainer()
model = try singleValueContainer.decode(Model.self)
}
}

let row = Row(["value": "foo", "otherValue": "bar", "actualValue": "test"])

let wrapper = try Wrapper<Struct>(row: row)
XCTAssertEqual(wrapper.model.value, "test")
XCTAssertEqual(wrapper.otherValue, "bar")
}
}