diff --git a/GRDB/Record/EncodableRecord+Encodable.swift b/GRDB/Record/EncodableRecord+Encodable.swift index c7816c6723..4ecbd5f803 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,80 @@ 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 { + 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 diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index b6aa66fa85..339a4a4973 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -83,6 +83,97 @@ 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") + } + } + + // 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