Skip to content

Commit 764ad4b

Browse files
authored
Merge pull request #1570 from groue/dev/single-value-encoding
Support single-value encoding
2 parents 78545f2 + e6d4223 commit 764ad4b

File tree

2 files changed

+166
-12
lines changed

2 files changed

+166
-12
lines changed

GRDB/Record/EncodableRecord+Encodable.swift

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,7 @@ private class RecordEncoder<Record: EncodableRecord>: Encoder {
3434
}
3535

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

5140
private struct KeyedContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
@@ -169,6 +158,80 @@ private class RecordEncoder<Record: EncodableRecord>: Encoder {
169158
}
170159
}
171160

161+
extension RecordEncoder: SingleValueEncodingContainer {
162+
private func unsupportedSingleValueEncoding() {
163+
fatalError("Can't encode a single value in a database row.")
164+
}
165+
166+
func encodeNil() throws {
167+
unsupportedSingleValueEncoding()
168+
}
169+
170+
func encode(_ value: Bool) throws {
171+
unsupportedSingleValueEncoding()
172+
}
173+
174+
func encode(_ value: String) throws {
175+
unsupportedSingleValueEncoding()
176+
}
177+
178+
func encode(_ value: Double) throws {
179+
unsupportedSingleValueEncoding()
180+
}
181+
182+
func encode(_ value: Float) throws {
183+
unsupportedSingleValueEncoding()
184+
}
185+
186+
func encode(_ value: Int) throws {
187+
unsupportedSingleValueEncoding()
188+
}
189+
190+
func encode(_ value: Int8) throws {
191+
unsupportedSingleValueEncoding()
192+
}
193+
194+
func encode(_ value: Int16) throws {
195+
unsupportedSingleValueEncoding()
196+
}
197+
198+
func encode(_ value: Int32) throws {
199+
unsupportedSingleValueEncoding()
200+
}
201+
202+
func encode(_ value: Int64) throws {
203+
unsupportedSingleValueEncoding()
204+
}
205+
206+
func encode(_ value: UInt) throws {
207+
unsupportedSingleValueEncoding()
208+
}
209+
210+
func encode(_ value: UInt8) throws {
211+
unsupportedSingleValueEncoding()
212+
}
213+
214+
func encode(_ value: UInt16) throws {
215+
unsupportedSingleValueEncoding()
216+
}
217+
218+
func encode(_ value: UInt32) throws {
219+
unsupportedSingleValueEncoding()
220+
}
221+
222+
func encode(_ value: UInt64) throws {
223+
unsupportedSingleValueEncoding()
224+
}
225+
226+
func encode<T>(_ value: T) throws where T : Encodable {
227+
if let record = value as? EncodableRecord {
228+
try record.encode(to: &_persistenceContainer)
229+
} else {
230+
try value.encode(to: self)
231+
}
232+
}
233+
}
234+
172235
// MARK: - ColumnEncoder
173236

174237
/// The encoder that encodes into a database column

Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,97 @@ extension MutablePersistableRecordEncodableTests {
8383
XCTAssertEqual(string, "foo (MutablePersistableRecord)")
8484
}
8585
}
86+
87+
// Regression test for <https://github.com/groue/GRDB.swift/issues/1565>
88+
func testSingleValueContainer() throws {
89+
struct Struct: Encodable {
90+
let value: String
91+
}
92+
93+
struct Wrapper<Model: Encodable>: MutablePersistableRecord, Encodable {
94+
static var databaseTableName: String { "t1" }
95+
var model: Model
96+
var otherValue: String
97+
98+
enum CodingKeys: String, CodingKey {
99+
case otherValue
100+
}
101+
102+
func encode(to encoder: any Encoder) throws {
103+
var modelContainer = encoder.singleValueContainer()
104+
try modelContainer.encode(model)
105+
106+
var container = encoder.container(keyedBy: CodingKeys.self)
107+
try container.encode(otherValue, forKey: .otherValue)
108+
}
109+
}
110+
111+
let dbQueue = try makeDatabaseQueue()
112+
try dbQueue.inDatabase { db in
113+
try db.create(table: "t1") { t in
114+
t.column("value", .text)
115+
t.column("otherValue", .text)
116+
}
117+
118+
var value = Wrapper(model: Struct(value: "foo"), otherValue: "bar")
119+
try assert(value, isEncodedIn: ["value": "foo", "otherValue": "bar"])
120+
121+
try value.insert(db)
122+
let row = try Row.fetchOne(db, sql: "SELECT value, otherValue FROM t1")!
123+
XCTAssertEqual(row[0], "foo")
124+
XCTAssertEqual(row[1], "bar")
125+
}
126+
}
127+
128+
// Regression test for <https://github.com/groue/GRDB.swift/issues/1565>
129+
// Here we test that `EncodableRecord` takes precedence over `Encodable`
130+
// when a record is encoded with a `SingleValueEncodingContainer`.
131+
func testSingleValueContainerWithEncodableRecord() throws {
132+
struct Struct: Encodable, EncodableRecord {
133+
let value: String
134+
135+
func encode(to container: inout PersistenceContainer) throws {
136+
container["column1"] = "test"
137+
container["column2"] = 12
138+
}
139+
}
140+
141+
struct Wrapper<Model: Encodable>: MutablePersistableRecord, Encodable {
142+
static var databaseTableName: String { "t1" }
143+
var model: Model
144+
var otherValue: String
145+
146+
enum CodingKeys: String, CodingKey {
147+
case otherValue
148+
}
149+
150+
func encode(to encoder: any Encoder) throws {
151+
var modelContainer = encoder.singleValueContainer()
152+
try modelContainer.encode(model)
153+
154+
var container = encoder.container(keyedBy: CodingKeys.self)
155+
try container.encode(otherValue, forKey: .otherValue)
156+
}
157+
}
158+
159+
let dbQueue = try makeDatabaseQueue()
160+
try dbQueue.inDatabase { db in
161+
try db.create(table: "t1") { t in
162+
t.column("column1", .text)
163+
t.column("column2", .integer)
164+
t.column("otherValue", .text)
165+
}
166+
167+
var value = Wrapper(model: Struct(value: "foo"), otherValue: "bar")
168+
try assert(value, isEncodedIn: ["column1": "test", "column2": 12, "otherValue": "bar"])
169+
170+
try value.insert(db)
171+
let row = try Row.fetchOne(db, sql: "SELECT column1, column2, otherValue FROM t1")!
172+
XCTAssertEqual(row[0], "test")
173+
XCTAssertEqual(row[1], 12)
174+
XCTAssertEqual(row[2], "bar")
175+
}
176+
}
86177
}
87178

88179
// MARK: - Different kinds of single-value properties

0 commit comments

Comments
 (0)