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 single-value encoding #1570

Merged
merged 3 commits into from
Jul 11, 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
87 changes: 75 additions & 12 deletions GRDB/Record/EncodableRecord+Encodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,7 @@ private class RecordEncoder<Record: EncodableRecord>: Encoder {
}

func singleValueContainer() -> SingleValueEncodingContainer {
// @itaiferber on https://forums.swift.org/t/how-to-encode-objects-of-unknown-type/12253/11
//
// > Encoding a value into a single-value container is equivalent to
// > encoding the value directly into the encoder, with the primary
// > difference being the above: encoding into the encoder writes the
// > contents of a type into the encoder, while encoding to a
// > single-value container gives the encoder a chance to intercept the
// > type as a whole.
//
// Wait for somebody hitting this fatal error so that we can write a
// meaningful regression test.
fatalError("single value encoding is not supported")
self
}

private struct KeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
Expand Down Expand Up @@ -169,6 +158,80 @@ private class RecordEncoder<Record: EncodableRecord>: Encoder {
}
}

extension RecordEncoder: SingleValueEncodingContainer {
private func unsupportedSingleValueEncoding() {
fatalError("Can't encode a single value in a database row.")
}

func encodeNil() throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Bool) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: String) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Double) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Float) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Int) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Int8) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Int16) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Int32) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: Int64) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: UInt) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: UInt8) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: UInt16) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: UInt32) throws {
unsupportedSingleValueEncoding()
}

func encode(_ value: UInt64) throws {
unsupportedSingleValueEncoding()
}

func encode<T>(_ value: T) throws where T : Encodable {
if let record = value as? EncodableRecord {
try record.encode(to: &_persistenceContainer)
} else {
try value.encode(to: self)
}
}
}

// MARK: - ColumnEncoder

/// The encoder that encodes into a database column
Expand Down
91 changes: 91 additions & 0 deletions Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,97 @@ extension MutablePersistableRecordEncodableTests {
XCTAssertEqual(string, "foo (MutablePersistableRecord)")
}
}

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

struct Wrapper<Model: Encodable>: MutablePersistableRecord, Encodable {
static var databaseTableName: String { "t1" }
var model: Model
var otherValue: String

enum CodingKeys: String, CodingKey {
case otherValue
}

func encode(to encoder: any Encoder) throws {
var modelContainer = encoder.singleValueContainer()
try modelContainer.encode(model)

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(otherValue, forKey: .otherValue)
}
}

let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("value", .text)
t.column("otherValue", .text)
}

var value = Wrapper(model: Struct(value: "foo"), otherValue: "bar")
try assert(value, isEncodedIn: ["value": "foo", "otherValue": "bar"])

try value.insert(db)
let row = try Row.fetchOne(db, sql: "SELECT value, otherValue FROM t1")!
XCTAssertEqual(row[0], "foo")
XCTAssertEqual(row[1], "bar")
}
}

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

func encode(to container: inout PersistenceContainer) throws {
container["column1"] = "test"
container["column2"] = 12
}
}

struct Wrapper<Model: Encodable>: MutablePersistableRecord, Encodable {
static var databaseTableName: String { "t1" }
var model: Model
var otherValue: String

enum CodingKeys: String, CodingKey {
case otherValue
}

func encode(to encoder: any Encoder) throws {
var modelContainer = encoder.singleValueContainer()
try modelContainer.encode(model)

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(otherValue, forKey: .otherValue)
}
}

let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.create(table: "t1") { t in
t.column("column1", .text)
t.column("column2", .integer)
t.column("otherValue", .text)
}

var value = Wrapper(model: Struct(value: "foo"), otherValue: "bar")
try assert(value, isEncodedIn: ["column1": "test", "column2": 12, "otherValue": "bar"])

try value.insert(db)
let row = try Row.fetchOne(db, sql: "SELECT column1, column2, otherValue FROM t1")!
XCTAssertEqual(row[0], "test")
XCTAssertEqual(row[1], 12)
XCTAssertEqual(row[2], "bar")
}
}
}

// MARK: - Different kinds of single-value properties
Expand Down