From 5999227c010488c91be410ce4a060339f3869cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Rou=C3=A9?= Date: Tue, 9 Jul 2024 13:31:54 +0200 Subject: [PATCH 1/3] Failing test for #1565 --- ...tablePersistableRecordEncodableTests.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index b6aa66fa85..9ccd862e18 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -83,6 +83,47 @@ extension MutablePersistableRecordEncodableTests { XCTAssertEqual(string, "foo (MutablePersistableRecord)") } } + + // Regression test for + func testSingleValueContainer() throws { + struct Struct: Encodable { + let value: String + } + + struct Wrapper: 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") + } + } } // MARK: - Different kinds of single-value properties From ba3c938eb9beb8734fbf6c790c18e01b1664fc11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Rou=C3=A9?= Date: Tue, 9 Jul 2024 13:32:06 +0200 Subject: [PATCH 2/3] Fix failing test for #1565 --- GRDB/Record/EncodableRecord+Encodable.swift | 83 ++++++++++++++++++--- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index c7816c6723..500a4814d4 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -34,18 +34,7 @@ private class RecordEncoder: 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: KeyedEncodingContainerProtocol { @@ -169,6 +158,76 @@ private class RecordEncoder: 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(_ value: T) throws where T : Encodable { + try value.encode(to: self) + } +} + // MARK: - ColumnEncoder /// The encoder that encodes into a database column From e6d4223b14cd1b9efa650923226ae43e12ed110f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Rou=C3=A9?= Date: Wed, 10 Jul 2024 08:47:49 +0200 Subject: [PATCH 3/3] EncodableRecord takes precedence over Encodable when a record is encoded with a SingleValueEncodingContainer. --- GRDB/Record/EncodableRecord+Encodable.swift | 6 ++- ...tablePersistableRecordEncodableTests.swift | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index 500a4814d4..4ecbd5f803 100644 --- a/GRDB/Record/EncodableRecord+Encodable.swift +++ b/GRDB/Record/EncodableRecord+Encodable.swift @@ -224,7 +224,11 @@ extension RecordEncoder: SingleValueEncodingContainer { } func encode(_ value: T) throws where T : Encodable { - try value.encode(to: self) + if let record = value as? EncodableRecord { + try record.encode(to: &_persistenceContainer) + } else { + try value.encode(to: self) + } } } diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index 9ccd862e18..339a4a4973 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -124,6 +124,56 @@ extension MutablePersistableRecordEncodableTests { XCTAssertEqual(row[1], "bar") } } + + // Regression test for + // 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: 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