-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Fix insertMany([Encodable]) failing due to asymmetric setters #1138
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,12 +86,22 @@ extension QueryType { | |
/// - Returns: An `INSERT` statement for the encodable objects | ||
public func insertMany(_ encodables: [Encodable], userInfo: [CodingUserInfoKey: Any] = [:], | ||
otherSetters: [Setter] = []) throws -> Insert { | ||
let combinedSetters = try encodables.map { encodable -> [Setter] in | ||
let encoder = SQLiteEncoder(userInfo: userInfo) | ||
let combinedSettersWithoutNils = try encodables.map { encodable -> [Setter] in | ||
let encoder = SQLiteEncoder(userInfo: userInfo, forcingNilValueSetters: false) | ||
try encodable.encode(to: encoder) | ||
return encoder.setters + otherSetters | ||
} | ||
return self.insertMany(combinedSetters) | ||
// requires the same number of setters per encodable | ||
guard Set(combinedSettersWithoutNils.map(\.count)).count == 1 else { | ||
// asymmetric sets of value insertions (some nil, some not), requires NULL value to satisfy INSERT query | ||
let combinedSymmetricSetters = try encodables.map { encodable -> [Setter] in | ||
let encoder = SQLiteEncoder(userInfo: userInfo, forcingNilValueSetters: true) | ||
try encodable.encode(to: encoder) | ||
return encoder.setters + otherSetters | ||
} | ||
return self.insertMany(combinedSymmetricSetters) | ||
} | ||
return self.insertMany(combinedSettersWithoutNils) | ||
} | ||
|
||
/// Creates an `INSERT ON CONFLICT DO UPDATE` statement, aka upsert, by encoding the given object | ||
|
@@ -165,9 +175,11 @@ private class SQLiteEncoder: Encoder { | |
|
||
let encoder: SQLiteEncoder | ||
let codingPath: [CodingKey] = [] | ||
let forcingNilValueSetters: Bool | ||
|
||
init(encoder: SQLiteEncoder) { | ||
init(encoder: SQLiteEncoder, forcingNilValueSetters: Bool = false) { | ||
self.encoder = encoder | ||
self.forcingNilValueSetters = forcingNilValueSetters | ||
} | ||
|
||
func superEncoder() -> Swift.Encoder { | ||
|
@@ -202,6 +214,46 @@ private class SQLiteEncoder: Encoder { | |
encoder.setters.append(Expression(key.stringValue) <- value) | ||
} | ||
|
||
func encodeIfPresent(_ value: Int?, forKey key: SQLiteEncoder.SQLiteKeyedEncodingContainer<Key>.Key) throws { | ||
if let value = value { | ||
encoder.setters.append(Expression(key.stringValue) <- value) | ||
} else if forcingNilValueSetters { | ||
encoder.setters.append(Expression<Int?>(key.stringValue) <- nil) | ||
} | ||
} | ||
|
||
func encodeIfPresent(_ value: Bool?, forKey key: Key) throws { | ||
if let value = value { | ||
encoder.setters.append(Expression(key.stringValue) <- value) | ||
} else if forcingNilValueSetters { | ||
encoder.setters.append(Expression<Bool?>(key.stringValue) <- nil) | ||
} | ||
} | ||
|
||
func encodeIfPresent(_ value: Float?, forKey key: Key) throws { | ||
if let value = value { | ||
encoder.setters.append(Expression(key.stringValue) <- Double(value)) | ||
} else if forcingNilValueSetters { | ||
encoder.setters.append(Expression<Double?>(key.stringValue) <- nil) | ||
} | ||
} | ||
|
||
func encodeIfPresent(_ value: Double?, forKey key: Key) throws { | ||
if let value = value { | ||
encoder.setters.append(Expression(key.stringValue) <- value) | ||
} else if forcingNilValueSetters { | ||
encoder.setters.append(Expression<Double?>(key.stringValue) <- nil) | ||
} | ||
} | ||
|
||
func encodeIfPresent(_ value: String?, forKey key: MyKey) throws { | ||
if let value = value { | ||
encoder.setters.append(Expression(key.stringValue) <- value) | ||
} else if forcingNilValueSetters { | ||
encoder.setters.append(Expression<String?>(key.stringValue) <- nil) | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. were all these previously automatically provided by the protocol? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I believe so. although this is not very clear from the docs. this behavior forces the setter to be NULL in the case where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. defined here: https://github.com/apple/swift/blob/main/stdlib/public/core/Codable.swift#L5752 it's a shame these have all to be overridden, but I guess there's no cleaner solution |
||
func encode<T>(_ value: T, forKey key: Key) throws where T: Swift.Encodable { | ||
switch value { | ||
case let data as Data: | ||
|
@@ -217,6 +269,28 @@ private class SQLiteEncoder: Encoder { | |
} | ||
} | ||
|
||
func encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T: Swift.Encodable { | ||
guard let value = value else { | ||
guard forcingNilValueSetters else { | ||
return | ||
} | ||
encoder.setters.append(Expression<String?>(key.stringValue) <- nil) | ||
return | ||
} | ||
switch value { | ||
case let data as Data: | ||
encoder.setters.append(Expression(key.stringValue) <- data) | ||
case let date as Date: | ||
encoder.setters.append(Expression(key.stringValue) <- date.datatypeValue) | ||
case let uuid as UUID: | ||
encoder.setters.append(Expression(key.stringValue) <- uuid.datatypeValue) | ||
default: | ||
let encoded = try JSONEncoder().encode(value) | ||
let string = String(data: encoded, encoding: .utf8) | ||
encoder.setters.append(Expression(key.stringValue) <- string) | ||
} | ||
} | ||
|
||
func encode(_ value: Int8, forKey key: Key) throws { | ||
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath, | ||
debugDescription: "encoding an Int8 is not supported")) | ||
|
@@ -274,9 +348,11 @@ private class SQLiteEncoder: Encoder { | |
fileprivate var setters: [Setter] = [] | ||
let codingPath: [CodingKey] = [] | ||
let userInfo: [CodingUserInfoKey: Any] | ||
let forcingNilValueSetters: Bool | ||
|
||
init(userInfo: [CodingUserInfoKey: Any]) { | ||
init(userInfo: [CodingUserInfoKey: Any], forcingNilValueSetters: Bool = false) { | ||
self.userInfo = userInfo | ||
self.forcingNilValueSetters = forcingNilValueSetters | ||
} | ||
|
||
func singleValueContainer() -> SingleValueEncodingContainer { | ||
|
@@ -288,7 +364,7 @@ private class SQLiteEncoder: Encoder { | |
} | ||
|
||
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey { | ||
KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self)) | ||
KeyedEncodingContainer(SQLiteKeyedEncodingContainer(encoder: self, forcingNilValueSetters: forcingNilValueSetters)) | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -365,21 +365,21 @@ class QueryTests: XCTestCase { | |
) | ||
} | ||
|
||
func test_insert_many_encodable() throws { | ||
func test_insert_many_encodables() throws { | ||
let emails = Table("emails") | ||
let value1 = TestCodable(int: 1, string: "2", bool: true, float: 3, double: 4, | ||
date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) | ||
let value2 = TestCodable(int: 2, string: "3", bool: true, float: 3, double: 5, | ||
date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) | ||
date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: "optional", sub: nil) | ||
let value3 = TestCodable(int: 3, string: "4", bool: true, float: 3, double: 6, | ||
date: Date(timeIntervalSince1970: 0), uuid: testUUIDValue, optional: nil, sub: nil) | ||
let insert = try emails.insertMany([value1, value2, value3]) | ||
assertSQL( | ||
""" | ||
INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"uuid\") | ||
VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F'), | ||
(2, '3', 1, 3.0, 5.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F'), | ||
(3, '4', 1, 3.0, 6.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F') | ||
INSERT INTO \"emails\" (\"int\", \"string\", \"bool\", \"float\", \"double\", \"date\", \"uuid\", \"optional\", \"sub\") | ||
VALUES (1, '2', 1, 3.0, 4.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', NULL, NULL), | ||
(2, '3', 1, 3.0, 5.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', 'optional', NULL), | ||
(3, '4', 1, 3.0, 6.0, '1970-01-01T00:00:00.000', 'E621E1F8-C36C-495A-93FC-0C247A3E6E5F', NULL, NULL) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the new codable decode behavior adds these NULLs correcting the issue |
||
""".replacingOccurrences(of: "\n", with: ""), | ||
insert | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this just delegate to
encode
like in the default protocol implementation?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this would be better, updating that