From feeedcb92dfa8941599ae458ad5893a47021d762 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Tue, 7 Aug 2018 15:29:16 +0100 Subject: [PATCH 01/37] Added nested Codable support by flattening into/from JSON --- GRDB/Record/FetchableRecord+Decodable.swift | 26 ++- GRDB/Record/PersistableRecord+Encodable.swift | 108 +++++++++- .../FetchableRecordDecodableTests.swift | 194 ++++++++++++++++++ Tests/GRDBTests/PersistableRecordTests.swift | 182 ++++++++++++++++ 4 files changed, 505 insertions(+), 5 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 44e510cfa2..105e92b962 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -1,3 +1,5 @@ +import Foundation + private struct RowKeyedDecodingContainer: KeyedDecodingContainerProtocol { let decoder: RowDecoder @@ -79,7 +81,17 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } else if dbValue.isNull { return nil } else { - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + do { + let natural = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + return natural + } catch { + if let data = row.dataNoCopy(named: key.stringValue), let dataString = String(data: data, encoding: .utf8), dataString.hasPrefix("[{") || dataString.hasPrefix("{"), dataString.hasSuffix("}]") || dataString.hasSuffix("}") { + // If data looks like JSON data then decode it into model (nested model as JSON) + return try JSONDecoder().decode(type.self, from: data) + } else { + fatalError("\(error)") + } + } } } @@ -116,7 +128,17 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // This allows decoding Date from String, or DatabaseValue from NULL. return type.fromDatabaseValue(dbValue) as! T } else { - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + do { + let natural = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + return natural + } catch { + if let data = row.dataNoCopy(named: key.stringValue), let dataString = String(data: data, encoding: .utf8), dataString.hasPrefix("[{") || dataString.hasPrefix("{"), dataString.hasSuffix("}]") || dataString.hasSuffix("}") { + // If data looks like JSON data then decode it into model (nested model as JSON) + return try JSONDecoder().decode(type.self, from: data) + } else { + fatalError("\(error)") + } + } } } diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index 135e8a149a..0cc002327a 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -1,3 +1,5 @@ +import Foundation + private struct PersistableRecordKeyedEncodingContainer : KeyedEncodingContainerProtocol { let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void @@ -40,7 +42,21 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn // This allows us to encode Date as String, for example. encode((value as! DatabaseValueConvertible), key.stringValue) } else { - try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) + do { + try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) + } catch { + // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON + let encodeError = error + do { + let json = try JSONEncoder().encode(value) + guard let modelAsString = String(data: json, encoding: .utf8) else { + throw JsonStringError.covertStringError("Error, could not make string out of JSON data") + } + return encode(modelAsString, key.stringValue) + } catch { + fatalError("Encode error: \(encodeError), tried to encode to Json, got error: \(error)") + } + } } } @@ -169,7 +185,7 @@ private struct PersistableRecordEncoder : Encoder { func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { // Asked for a keyed type: top level required guard codingPath.isEmpty else { - fatalError("unkeyed encoding is not supported") + return KeyedEncodingContainer(ThrowingKeyedContainer(error: EncodingError.invalidValue(codingPath.isEmpty, EncodingError.Context(codingPath: codingPath, debugDescription: "unkeyed encoding is not supported")))) } return KeyedEncodingContainer(PersistableRecordKeyedEncodingContainer(encode: encode)) } @@ -180,7 +196,7 @@ private struct PersistableRecordEncoder : Encoder { /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. func unkeyedContainer() -> UnkeyedEncodingContainer { - fatalError("unkeyed encoding is not supported") + return ThrowingUnkeyedContainer(error: EncodingError.invalidValue(encode, EncodingError.Context(codingPath: [], debugDescription: "unkeyed encoding is not supported"))) } /// Returns an encoding container appropriate for holding a single primitive value. @@ -194,6 +210,92 @@ private struct PersistableRecordEncoder : Encoder { } } +class ThrowingKeyedContainer: KeyedEncodingContainerProtocol { + let errorMessage: Error + var codingPath: [CodingKey] = [] + + init(error: Error) { + errorMessage = error + } + + func encodeNil(forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Bool, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Int, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Int8, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Int16, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Int32, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Int64, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: UInt, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: UInt8, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: UInt16, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: UInt32, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: UInt64, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Float, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: Double, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: String, forKey key: KeyType) throws { throw errorMessage } + func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw errorMessage } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError("Not implemented") + } + func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { + fatalError("Not implemented") + } + func superEncoder() -> Encoder { + fatalError("Not implemented") + } + + func superEncoder(forKey key: KeyType) -> Encoder { + fatalError("Not implemented") + } +} + +class ThrowingUnkeyedContainer: UnkeyedEncodingContainer { + let errorMessage: Error + var codingPath: [CodingKey] = [] + var count: Int = 0 + + init(error: Error) { + errorMessage = error + } + + func encode(_ value: Int) throws { throw errorMessage } + func encode(_ value: Int8) throws { throw errorMessage } + func encode(_ value: Int16) throws { throw errorMessage } + func encode(_ value: Int32) throws { throw errorMessage } + func encode(_ value: Int64) throws { throw errorMessage } + func encode(_ value: UInt) throws { throw errorMessage } + func encode(_ value: UInt8) throws { throw errorMessage } + func encode(_ value: UInt16) throws { throw errorMessage } + func encode(_ value: UInt32) throws { throw errorMessage } + func encode(_ value: UInt64) throws { throw errorMessage } + func encode(_ value: Float) throws { throw errorMessage } + func encode(_ value: Double) throws { throw errorMessage } + func encode(_ value: String) throws { throw errorMessage } + func encode(_ value: T) throws where T : Encodable { throw errorMessage } + func encode(_ value: Bool) throws { throw errorMessage } + func encodeNil() throws { throw errorMessage } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError("Not implemented") + + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + fatalError("Not implemented") + + } + + func superEncoder() -> Encoder { + fatalError("Not implemented") + + } +} + +enum JsonStringError: Error { + case covertStringError(String) +} + extension MutablePersistableRecord where Self: Encodable { public func encode(to container: inout PersistenceContainer) { // The inout container parameter won't enter an escaping closure since diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 03ab933121..d72c1222d7 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -301,3 +301,197 @@ extension FetchableRecordDecodableTests { XCTAssertEqual(value.uuid, uuid) } } + +// MARK: - Custom nested Decodable types - nested saved as JSON + +extension FetchableRecordDecodableTests { + func testOptionalNestedStruct() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: NestedStruct? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: NestedStruct()) + try value.insert(db) + + let parentModel = try StructWithNestedType.fetchAll(db) + + guard let nestedModel = parentModel.first?.nested else { + XCTFail() + return + } + + // Check the nested model contains the expected values of first and last name + XCTAssertEqual(nestedModel.firstName, "Bob") + XCTAssertEqual(nestedModel.lastName, "Dylan") + } + } + + func testOptionalNestedStructNil() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: NestedStruct? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: nil) + try value.insert(db) + + let parentModel = try StructWithNestedType.fetchAll(db) + + XCTAssertNil(parentModel.first?.nested) + } + } + + func testOptionalNestedArrayStruct() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: [NestedStruct]? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) + try value.insert(db) + + let parentModel = try StructWithNestedType.fetchAll(db) + + guard let arrayOfNestedModel = parentModel.first?.nested, let firstNestedModelInArray = arrayOfNestedModel.first else { + XCTFail() + return + } + + // Check there are two models in array + XCTAssertTrue(arrayOfNestedModel.count == 2) + + // Check the nested model contains the expected values of first and last name + XCTAssertEqual(firstNestedModelInArray.firstName, "Bob") + XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan") + } + } + + func testOptionalNestedArrayStructNil() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: [NestedStruct]? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: nil) + try value.insert(db) + + let parentModel = try StructWithNestedType.fetchAll(db) + + XCTAssertNil(parentModel.first?.nested) + } + } + + func testNonOptionalNestedStruct() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: NestedStruct + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: NestedStruct()) + try value.insert(db) + + let parentModel = try StructWithNestedType.fetchAll(db) + + guard let nestedModel = parentModel.first?.nested else { + XCTFail() + return + } + + // Check the nested model contains the expected values of first and last name + XCTAssertEqual(nestedModel.firstName, "Bob") + XCTAssertEqual(nestedModel.lastName, "Dylan") + } + } + + func testNonOptionalNestedArrayStruct() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: [NestedStruct] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) + try value.insert(db) + + let parentModel = try StructWithNestedType.fetchAll(db) + + guard let arrayOfNestedModel = parentModel.first?.nested, let firstNestedModelInArray = arrayOfNestedModel.first else { + XCTFail() + return + } + + // Check there are two models in array + XCTAssertTrue(arrayOfNestedModel.count == 2) + + // Check the nested model contains the expected values of first and last name + XCTAssertEqual(firstNestedModelInArray.firstName, "Bob") + XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan") + } + } +} diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index 3d101212f9..3853d0ad93 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -761,3 +761,185 @@ class PersistableRecordTests: GRDBTestCase { } } } + +// MARK: - Custom nested Codable types - nested saved as JSON + +extension PersistableRecordTests { + + func testOptionalNestedStruct() throws { + struct NestedStruct : PersistableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, Codable { + static let databaseTableName = "t1" + let nested: NestedStruct? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: NestedStruct()) + try value.insert(db) + + let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! + + // Encodable has a default implementation which encodes a model to JSON as String. + // We expect here JSON in the form of a String + XCTAssert(dbValue.storage.value is String) + let string = dbValue.storage.value as! String + if let data = string.data(using: .utf8) { + do { + let decoded = try JSONDecoder().decode(NestedStruct.self, from: data) + XCTAssertEqual(NestedStruct().firstName, decoded.firstName) + XCTAssertEqual(NestedStruct().lastName, decoded.lastName) + } catch { + XCTFail(error.localizedDescription) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } + + func testOptionalNestedStructNil() throws { + struct NestedStruct : PersistableRecord, Encodable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, Encodable { + static let databaseTableName = "t1" + let nested: NestedStruct? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: nil) + try value.insert(db) + + let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! + + // We expect here nil + XCTAssertNil(dbValue.storage.value) + } + } + + func testOptionalNestedArrayStruct() throws { + struct NestedStruct : PersistableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, Codable { + static let databaseTableName = "t1" + let nested: [NestedStruct]? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) + try value.insert(db) + + let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! + + // Encodable has a default implementation which encodes a model to JSON as String. + // We expect here JSON in the form of a String + XCTAssert(dbValue.storage.value is String) + let string = dbValue.storage.value as! String + if let data = string.data(using: .utf8) { + do { + let decoded = try JSONDecoder().decode([NestedStruct].self, from: data) + XCTAssertEqual(decoded.count, 2) + XCTAssertEqual(NestedStruct().firstName, decoded.first!.firstName) + XCTAssertEqual(NestedStruct().lastName, decoded.first!.lastName) + XCTAssertEqual(NestedStruct().firstName, decoded.last!.firstName) + XCTAssertEqual(NestedStruct().lastName, decoded.last!.lastName) + } catch { + XCTFail(error.localizedDescription) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } + + func testOptionalNestedArrayStructNil() throws { + struct NestedStruct : PersistableRecord, Encodable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, Encodable { + static let databaseTableName = "t1" + let nested: [NestedStruct]? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: nil) + try value.insert(db) + + let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! + + // We expect here nil + XCTAssertNil(dbValue.storage.value) + } + } + + func testNonOptionalNestedStruct() throws { + struct NestedStruct : PersistableRecord, Codable { + let firstName = "Bob" + let lastName = "Dylan" + } + + struct StructWithNestedType : PersistableRecord, Codable { + static let databaseTableName = "t1" + let nested: NestedStruct + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let value = StructWithNestedType(nested: NestedStruct()) + try value.insert(db) + + let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! + + // Encodable has a default implementation which encodes a model to JSON as String. + // We expect here JSON in the form of a String + XCTAssert(dbValue.storage.value is String) + let string = dbValue.storage.value as! String + if let data = string.data(using: .utf8) { + do { + let decoded = try JSONDecoder().decode(NestedStruct.self, from: data) + XCTAssertEqual(NestedStruct().firstName, decoded.firstName) + XCTAssertEqual(NestedStruct().lastName, decoded.lastName) + } catch { + XCTFail(error.localizedDescription) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } +} From 74aae3dd594239849b510fa83863579344672a6b Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Wed, 8 Aug 2018 11:41:26 +0100 Subject: [PATCH 02/37] implemented requested changes --- GRDB/Record/FetchableRecord+Decodable.swift | 40 +++++++----- GRDB/Record/PersistableRecord+Encodable.swift | 14 ++--- .../FetchableRecordDecodableTests.swift | 55 +++++++++-------- Tests/GRDBTests/PersistableRecordTests.swift | 61 ++++++++++--------- 4 files changed, 94 insertions(+), 76 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 105e92b962..43dcc04584 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -82,14 +82,20 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer return nil } else { do { - let natural = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) - return natural + // This decoding will fail for types that need a keyed container, + // because we're decoding a database value here (string, int, double, data, null, Codable) + return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } catch { - if let data = row.dataNoCopy(named: key.stringValue), let dataString = String(data: data, encoding: .utf8), dataString.hasPrefix("[{") || dataString.hasPrefix("{"), dataString.hasSuffix("}]") || dataString.hasSuffix("}") { - // If data looks like JSON data then decode it into model (nested model as JSON) - return try JSONDecoder().decode(type.self, from: data) - } else { - fatalError("\(error)") + switch error as! DecodingError { + case .typeMismatch(_, let context): + if context.debugDescription == "unkeyed decoding is not supported", let data = row.dataNoCopy(named: key.stringValue) { + // Support for keyed containers ( [Codable] ) + return try JSONDecoder().decode(type.self, from: data) + } else { + throw(error) + } + default: + throw(error) } } } @@ -129,14 +135,20 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer return type.fromDatabaseValue(dbValue) as! T } else { do { - let natural = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) - return natural + // This decoding will fail for types that need a keyed container, + // because we're decoding a database value here (string, int, double, data, null, Codable) + return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } catch { - if let data = row.dataNoCopy(named: key.stringValue), let dataString = String(data: data, encoding: .utf8), dataString.hasPrefix("[{") || dataString.hasPrefix("{"), dataString.hasSuffix("}]") || dataString.hasSuffix("}") { - // If data looks like JSON data then decode it into model (nested model as JSON) - return try JSONDecoder().decode(type.self, from: data) - } else { - fatalError("\(error)") + switch error as! DecodingError { + case .typeMismatch(_, let context): + if context.debugDescription == "unkeyed decoding is not supported", let data = row.dataNoCopy(named: key.stringValue) { + // Support for keyed containers ( [Codable] ) + return try JSONDecoder().decode(type.self, from: data) + } else { + throw(error) + } + default: + throw(error) } } } diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index 0cc002327a..07ba5cfb68 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -46,15 +46,13 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) } catch { // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON - let encodeError = error do { let json = try JSONEncoder().encode(value) - guard let modelAsString = String(data: json, encoding: .utf8) else { - throw JsonStringError.covertStringError("Error, could not make string out of JSON data") - } + //the Data from the encoder is guaranteed to convert to String + let modelAsString = String(data: json, encoding: .utf8)! return encode(modelAsString, key.stringValue) } catch { - fatalError("Encode error: \(encodeError), tried to encode to Json, got error: \(error)") + throw(error) } } } @@ -185,7 +183,8 @@ private struct PersistableRecordEncoder : Encoder { func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { // Asked for a keyed type: top level required guard codingPath.isEmpty else { - return KeyedEncodingContainer(ThrowingKeyedContainer(error: EncodingError.invalidValue(codingPath.isEmpty, EncodingError.Context(codingPath: codingPath, debugDescription: "unkeyed encoding is not supported")))) + let error = EncodingError.invalidValue(encode, EncodingError.Context(codingPath: codingPath, debugDescription: "unkeyed encoding is not supported")) + return KeyedEncodingContainer(ThrowingKeyedContainer(error: error)) } return KeyedEncodingContainer(PersistableRecordKeyedEncodingContainer(encode: encode)) } @@ -196,7 +195,8 @@ private struct PersistableRecordEncoder : Encoder { /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. func unkeyedContainer() -> UnkeyedEncodingContainer { - return ThrowingUnkeyedContainer(error: EncodingError.invalidValue(encode, EncodingError.Context(codingPath: [], debugDescription: "unkeyed encoding is not supported"))) + let error = EncodingError.invalidValue(encode, EncodingError.Context(codingPath: [], debugDescription: "unkeyed encoding is not supported")) + return ThrowingUnkeyedContainer(error: error) } /// Returns an encoding container appropriate for holding a single primitive value. diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index d72c1222d7..f6745168d3 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -306,9 +306,9 @@ extension FetchableRecordDecodableTests { extension FetchableRecordDecodableTests { func testOptionalNestedStruct() throws { - struct NestedStruct : PersistableRecord, FetchableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { @@ -321,8 +321,8 @@ extension FetchableRecordDecodableTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: NestedStruct()) + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: nested) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) @@ -339,9 +339,9 @@ extension FetchableRecordDecodableTests { } func testOptionalNestedStructNil() throws { - struct NestedStruct : PersistableRecord, FetchableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { @@ -365,9 +365,9 @@ extension FetchableRecordDecodableTests { } func testOptionalNestedArrayStruct() throws { - struct NestedStruct : PersistableRecord, FetchableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { @@ -380,8 +380,9 @@ extension FetchableRecordDecodableTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: [nested, nested]) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) @@ -401,9 +402,9 @@ extension FetchableRecordDecodableTests { } func testOptionalNestedArrayStructNil() throws { - struct NestedStruct : PersistableRecord, FetchableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct: Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { @@ -427,9 +428,9 @@ extension FetchableRecordDecodableTests { } func testNonOptionalNestedStruct() throws { - struct NestedStruct : PersistableRecord, FetchableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct: Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { @@ -442,8 +443,9 @@ extension FetchableRecordDecodableTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: NestedStruct()) + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: nested) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) @@ -460,9 +462,9 @@ extension FetchableRecordDecodableTests { } func testNonOptionalNestedArrayStruct() throws { - struct NestedStruct : PersistableRecord, FetchableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { @@ -475,8 +477,9 @@ extension FetchableRecordDecodableTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: [nested, nested]) try value.insert(db) let parentModel = try StructWithNestedType.fetchAll(db) diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index 3853d0ad93..7e681b4de0 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -767,9 +767,9 @@ class PersistableRecordTests: GRDBTestCase { extension PersistableRecordTests { func testOptionalNestedStruct() throws { - struct NestedStruct : PersistableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, Codable { @@ -782,8 +782,9 @@ extension PersistableRecordTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: NestedStruct()) + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: nested) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! @@ -795,8 +796,8 @@ extension PersistableRecordTests { if let data = string.data(using: .utf8) { do { let decoded = try JSONDecoder().decode(NestedStruct.self, from: data) - XCTAssertEqual(NestedStruct().firstName, decoded.firstName) - XCTAssertEqual(NestedStruct().lastName, decoded.lastName) + XCTAssertEqual(nested.firstName, decoded.firstName) + XCTAssertEqual(nested.lastName, decoded.lastName) } catch { XCTFail(error.localizedDescription) } @@ -807,9 +808,9 @@ extension PersistableRecordTests { } func testOptionalNestedStructNil() throws { - struct NestedStruct : PersistableRecord, Encodable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Encodable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, Encodable { @@ -834,9 +835,9 @@ extension PersistableRecordTests { } func testOptionalNestedArrayStruct() throws { - struct NestedStruct : PersistableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, Codable { @@ -849,8 +850,9 @@ extension PersistableRecordTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: [NestedStruct(), NestedStruct()]) + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: [nested, nested]) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! @@ -863,10 +865,10 @@ extension PersistableRecordTests { do { let decoded = try JSONDecoder().decode([NestedStruct].self, from: data) XCTAssertEqual(decoded.count, 2) - XCTAssertEqual(NestedStruct().firstName, decoded.first!.firstName) - XCTAssertEqual(NestedStruct().lastName, decoded.first!.lastName) - XCTAssertEqual(NestedStruct().firstName, decoded.last!.firstName) - XCTAssertEqual(NestedStruct().lastName, decoded.last!.lastName) + XCTAssertEqual(nested.firstName, decoded.first!.firstName) + XCTAssertEqual(nested.lastName, decoded.first!.lastName) + XCTAssertEqual(nested.firstName, decoded.last!.firstName) + XCTAssertEqual(nested.lastName, decoded.last!.lastName) } catch { XCTFail(error.localizedDescription) } @@ -877,9 +879,9 @@ extension PersistableRecordTests { } func testOptionalNestedArrayStructNil() throws { - struct NestedStruct : PersistableRecord, Encodable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Encodable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, Encodable { @@ -904,9 +906,9 @@ extension PersistableRecordTests { } func testNonOptionalNestedStruct() throws { - struct NestedStruct : PersistableRecord, Codable { - let firstName = "Bob" - let lastName = "Dylan" + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? } struct StructWithNestedType : PersistableRecord, Codable { @@ -919,8 +921,9 @@ extension PersistableRecordTests { try db.create(table: "t1") { t in t.column("nested", .text) } - - let value = StructWithNestedType(nested: NestedStruct()) + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: nested) try value.insert(db) let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! @@ -932,8 +935,8 @@ extension PersistableRecordTests { if let data = string.data(using: .utf8) { do { let decoded = try JSONDecoder().decode(NestedStruct.self, from: data) - XCTAssertEqual(NestedStruct().firstName, decoded.firstName) - XCTAssertEqual(NestedStruct().lastName, decoded.lastName) + XCTAssertEqual(nested.firstName, decoded.firstName) + XCTAssertEqual(nested.lastName, decoded.lastName) } catch { XCTFail(error.localizedDescription) } From 7def60c3c0d99482759b39bbeb3d7cca41745c60 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Wed, 8 Aug 2018 14:35:05 +0100 Subject: [PATCH 03/37] fixed checking for nested codable --- GRDB/Record/FetchableRecord+Decodable.swift | 34 +++++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 43dcc04584..b1f0e1d0d4 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -83,14 +83,21 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } else { do { // This decoding will fail for types that need a keyed container, - // because we're decoding a database value here (string, int, double, data, null, Codable) - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + // because we're decoding a database value here (string, int, double, data, null) + // if we find a nested Decodable type then pass the string to JSON decoder + let value = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + if let _ = value as? Codable { + // Support for nested ( Codable ) + return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) + } else { + return value + } } catch { switch error as! DecodingError { case .typeMismatch(_, let context): - if context.debugDescription == "unkeyed decoding is not supported", let data = row.dataNoCopy(named: key.stringValue) { - // Support for keyed containers ( [Codable] ) - return try JSONDecoder().decode(type.self, from: data) + if context.debugDescription == "unkeyed decoding is not supported" { + // Support for nested keyed containers ( [Codable] ) + return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) } else { throw(error) } @@ -136,14 +143,21 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } else { do { // This decoding will fail for types that need a keyed container, - // because we're decoding a database value here (string, int, double, data, null, Codable) - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + // because we're decoding a database value here (string, int, double, data, null) + // if we find a nested Decodable type then pass the string to JSON decoder + let value = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + if let _ = value as? Codable { + // Support for nested ( Codable ) + return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) + } else { + return value + } } catch { switch error as! DecodingError { case .typeMismatch(_, let context): - if context.debugDescription == "unkeyed decoding is not supported", let data = row.dataNoCopy(named: key.stringValue) { - // Support for keyed containers ( [Codable] ) - return try JSONDecoder().decode(type.self, from: data) + if context.debugDescription == "unkeyed decoding is not supported" { + // Support for nested keyed containers ( [Codable] ) + return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) } else { throw(error) } From 2826472c1986b77717b70e0353f635b0990894be Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Thu, 9 Aug 2018 09:46:45 +0100 Subject: [PATCH 04/37] sorted encoder outputFormatting for ios11+ etcl --- GRDB/Record/PersistableRecord+Encodable.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index 07ba5cfb68..c0259d957c 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -47,7 +47,11 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn } catch { // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON do { - let json = try JSONEncoder().encode(value) + let encoder = JSONEncoder() + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys + } + let json = try encoder.encode(value) //the Data from the encoder is guaranteed to convert to String let modelAsString = String(data: json, encoding: .utf8)! return encode(modelAsString, key.stringValue) From 6262d10794f08350ff284ae667f1fe2e60bd2c99 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Tue, 14 Aug 2018 09:25:44 +0100 Subject: [PATCH 05/37] added catch --- GRDB/Record/PersistableRecord+Encodable.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index c0259d957c..9308326b7d 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -158,7 +158,23 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { // This allows us to encode Date as String, for example. encode(dbValueConvertible.databaseValue, key.stringValue) } else { - try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) + do { + try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) + } catch { + // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON + do { + let encoder = JSONEncoder() + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys + } + let json = try encoder.encode(value) + //the Data from the encoder is guaranteed to convert to String + let modelAsString = String(data: json, encoding: .utf8)! + return encode(modelAsString, key.stringValue) + } catch { + throw(error) + } + } } } } From 7f16e357d0ffc7a3337924eb506899fd43cc3576 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Tue, 14 Aug 2018 10:39:31 +0100 Subject: [PATCH 06/37] extended Codable documentation and added test for the example used in the documentation --- README.md | 19 +++++-- .../FetchableRecordDecodableTests.swift | 52 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4241e57132..47836e6202 100644 --- a/README.md +++ b/README.md @@ -2567,15 +2567,25 @@ struct Link : PersistableRecord { ## Codable Records -[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4. +[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4, GRDB supports all Codable conforming types with the exception of Dictionaries. GRDB provides default implementations for [`FetchableRecord.init(row:)`](#fetchablerecord-protocol) and [`PersistableRecord.encode(to:)`](#persistablerecord-protocol) for record types that also adopt an archival protocol (`Codable`, `Encodable` or `Decodable`). When all their properties are themselves codable, Swift generates the archiving methods, and you don't need to write them down: ```swift -// Declare a plain Codable struct or class... +// Declare a Codable struct or class, nested Codable objects as well as Sets, Arrays, and Optionals are supported with the exception of Dictionaries struct Player: Codable { let name: String let score: Int + let scores: [Int] + let lastMedal: PlayerMedal + let medals: [PlayerMedal] + //let timeline: [String: PlayerMedal] // <- Conforms to Codable but is not supported by GRDB + } + +// A simple Codable that will be nested in a parent Codable +struct PlayerMedal : Codable { + let name: String + let type: String } // Adopt Record protocols... @@ -2588,13 +2598,12 @@ try dbQueue.write { db in } ``` -GRDB support for Codable works well with "flat" records, whose stored properties are all simple [values](#values) (Bool, Int, String, Date, Swift enums, etc.) For example, the following record is not flat: +GRDB support for Codable works with standard library types like String, Int, and Double; and Foundation types like Date, Data, and URL, Apple lists conformance for : (Array, CGAffineTransform, CGPoint, CGRect, CGSize, CGVector, DateInterval, Decimal, Dictionary, MPMusicPlayerPlayParameters, Optional, Set) Anything else like say a CLLocationCoordinate2D will break conformity e.g.: ```swift -// Can't take profit from Codable code generation: struct Place: FetchableRecord, PersistableRecord, Codable { var title: String - var coordinate: CLLocationCoordinate2D // <- Not a simple value! + var coordinate: CLLocationCoordinate2D // <- Does not conform to Codable } ``` diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index f6745168d3..b2c6fb7db4 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -497,4 +497,56 @@ extension FetchableRecordDecodableTests { XCTAssertEqual(firstNestedModelInArray.lastName, "Dylan") } } + + func testCodableExampleCode() throws { + struct Player: PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let name: String + let score: Int + let scores: [Int] + let lastMedal: PlayerMedal + let medals: [PlayerMedal] + //let timeline: [String: PlayerMedal] // <- Conforms to Codable but is not supported by GRDB + } + + // A simple Codable that will be nested in a parent Codable + struct PlayerMedal : Codable { + let name: String? + let type: String? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("name", .text) + t.column("score", .integer) + t.column("scores", .integer) + t.column("lastMedal", .text) + t.column("medals", .text) + //t.column("timeline", .text) + } + + let medal1 = PlayerMedal(name: "First", type: "Gold") + let medal2 = PlayerMedal(name: "Second", type: "Silver") + //let timeline = ["Local Contest":medal1, "National Contest":medal2] + let value = Player(name: "PlayerName", score: 10, scores: [1,2,3,4,5], lastMedal: medal1, medals: [medal1, medal2]) + try value.insert(db) + + let parentModel = try Player.fetchAll(db) + + guard let arrayOfNestedModel = parentModel.first?.medals, let firstNestedModelInArray = arrayOfNestedModel.first, let secondNestedModelInArray = arrayOfNestedModel.last else { + XCTFail() + return + } + + // Check there are two models in array + XCTAssertTrue(arrayOfNestedModel.count == 2) + + // Check the nested model contains the expected values of first and last name + XCTAssertEqual(firstNestedModelInArray.name, "First") + XCTAssertEqual(secondNestedModelInArray.name, "Second") + } + + } + } From 8c06e7bff58a458978739e1a256c73b0ead2a3db Mon Sep 17 00:00:00 2001 From: Angus Muller Date: Tue, 14 Aug 2018 11:16:47 +0100 Subject: [PATCH 07/37] Detached rows string to data and tests --- GRDB/Core/Support/Foundation/Data.swift | 11 ++ .../FetchableRecordDecodableTests.swift | 143 ++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift index c88608bf6a..3f2e4af679 100644 --- a/GRDB/Core/Support/Foundation/Data.swift +++ b/GRDB/Core/Support/Foundation/Data.swift @@ -25,6 +25,17 @@ extension Data : DatabaseValueConvertible, StatementColumnConvertible { /// a Blob. public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { guard case .blob(let data) = dbValue.storage else { + // Check to see if data is String, if so then pass is through JSONSerialization + // to confirm if contains JSON, if so then this is a nested type stored as JSON, + // so return string as Data so that it can be decoded + if let valueIsString = dbValue.storage.value as? String, let dataUtf8 = valueIsString.data(using: .utf8) { + do { + try JSONSerialization.jsonObject(with: dataUtf8, options: []) + return dataUtf8 + } catch { + return nil + } + } return nil } return data diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index b2c6fb7db4..ddb2cf3011 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -548,5 +548,148 @@ extension FetchableRecordDecodableTests { } } + + // MARK: - JSON data in Detahced Rows + + func testDetachedRows() throws { + struct NestedStruct : PersistableRecord, FetchableRecord, Codable { + let firstName: String? + let lastName: String? + } + + struct StructWithNestedType : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let nested: NestedStruct + } + + let row: Row = ["nested": """ + {"firstName":"Bob","lastName":"Dylan"} + """] + + let model = StructWithNestedType(row: row) + XCTAssertEqual(model.nested.firstName, "Bob") + XCTAssertEqual(model.nested.lastName, "Dylan") + } + + func testArrayOfDetachedRowsAsData() throws { + struct TestStruct : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let name: String + } + + let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" + let jsonAsData = jsonAsString.data(using: .utf8) + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("name", .text) + } + + let model = TestStruct(name: jsonAsString) + try model.insert(db) + } + + try dbQueue.read { db in + + // ... with an array of detached rows: + let array = try Row.fetchAll(db, "SELECT * FROM t1") + for row in array { + let data1: Data? = row["name"] + XCTAssertEqual(jsonAsData, data1) + let data = row.dataNoCopy(named: "name") + XCTAssertEqual(jsonAsData, data) + } + } + } + + func testArrayOfDetachedRowsAsString() throws { + struct TestStruct : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let name: String + } + + let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("name", .text) + } + + let model = TestStruct(name: jsonAsString) + try model.insert(db) + } + + try dbQueue.read { db in + + // ... with an array of detached rows: + let array = try Row.fetchAll(db, "SELECT * FROM t1") + for row in array { + let string: String? = row["name"] + XCTAssertEqual(jsonAsString, string) + } + } + } + + func testCursorRowsAsData() throws { + struct TestStruct : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let name: String + } + + let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" + let jsonAsData = jsonAsString.data(using: .utf8) + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("name", .text) + } + + let model = TestStruct(name: jsonAsString) + try model.insert(db) + } + + try dbQueue.read { db in + // Compare cursor of low-level rows: + let cursor = try Row.fetchCursor(db, "SELECT * FROM t1") + while let row = try cursor.next() { + let data1: Data? = row["name"] + XCTAssertEqual(jsonAsData, data1) + let data = row.dataNoCopy(named: "name") + XCTAssertEqual(jsonAsData, data) + } + } + } + + func testCursorRowsAsString() throws { + struct TestStruct : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let name: String + } + + let jsonAsString = "{\"firstName\":\"Bob\",\"lastName\":\"Marley\"}" + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("name", .text) + } + + let model = TestStruct(name: jsonAsString) + try model.insert(db) + } + + try dbQueue.read { db in + // Compare cursor of low-level rows: + let cursor = try Row.fetchCursor(db, "SELECT * FROM t1") + while let row = try cursor.next() { + let string: String? = row["name"] + XCTAssertEqual(jsonAsString, string) + } + } + } + } From a08974ffa447da56872f15ee0bf4e6c7577db7c9 Mon Sep 17 00:00:00 2001 From: Angus Muller Date: Tue, 14 Aug 2018 11:20:02 +0100 Subject: [PATCH 08/37] Added tests for arrays --- Tests/GRDBTests/PersistableRecordTests.swift | 101 ++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index 7e681b4de0..3314612aaf 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -907,8 +907,8 @@ extension PersistableRecordTests { func testNonOptionalNestedStruct() throws { struct NestedStruct : Codable { - let firstName: String? - let lastName: String? + let firstName: String + let lastName: String } struct StructWithNestedType : PersistableRecord, Codable { @@ -945,4 +945,101 @@ extension PersistableRecordTests { } } } + + func testNonOptionalNestedArrayStruct() throws { + struct NestedStruct : Codable { + let firstName: String + let lastName: String + } + + struct StructWithNestedType : PersistableRecord, Codable { + static let databaseTableName = "t1" + let nested: [NestedStruct] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("nested", .text) + } + + let nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: [nested]) + try value.insert(db) + + let dbValue = try DatabaseValue.fetchOne(db, "SELECT nested FROM t1")! + + // Encodable has a default implementation which encodes a model to JSON as String. + // We expect here JSON in the form of a String + XCTAssert(dbValue.storage.value is String) + let string = dbValue.storage.value as! String + if let data = string.data(using: .utf8) { + do { + let decoded = try JSONDecoder().decode([NestedStruct].self, from: data) + XCTAssertEqual(nested.firstName, decoded.first!.firstName) + XCTAssertEqual(nested.lastName, decoded.first!.lastName) + } catch { + XCTFail(error.localizedDescription) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } + + func testStringStoredInArray() throws { + struct TestStruct : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let numbers: [String] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("numbers", .text) + } + + let model = TestStruct(numbers: ["test1", "test2", "test3"]) + try model.insert(db) + + // Encodable has a default implementation which encodes a model to JSON as String. + // We expect here JSON in the form of a String + + guard let fetchModel = try TestStruct.fetchOne(db) else { + XCTFail("Could not find record in db") + return + } + + print(fetchModel.numbers.first!) + XCTAssertEqual(model.numbers, fetchModel.numbers) + } + } + + func testOptionalStringStoredInArray() throws { + struct TestStruct : PersistableRecord, FetchableRecord, Codable { + static let databaseTableName = "t1" + let numbers: [String]? + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "t1") { t in + t.column("numbers", .text) + } + + let model = TestStruct(numbers: ["test1", "test2", "test3"]) + try model.insert(db) + + // Encodable has a default implementation which encodes a model to JSON as String. + // We expect here JSON in the form of a String + + guard let fetchModel = try TestStruct.fetchOne(db) else { + XCTFail("Could not find record in db") + return + } + + XCTAssertEqual(model.numbers, fetchModel.numbers) + } + } + } From d81475adc4249d2fb17456efa2d60bbdb92e4947 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Tue, 14 Aug 2018 13:06:26 +0100 Subject: [PATCH 09/37] 6262d10 continued --- GRDB/Record/PersistableRecord+Encodable.swift | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index 9308326b7d..e7f4005b05 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -46,16 +46,22 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) } catch { // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON - do { - let encoder = JSONEncoder() - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - encoder.outputFormatting = .sortedKeys + switch error as! EncodingError { + case .invalidValue(_, let context): + if context.debugDescription == "unkeyed encoding is not supported" { + // Support for keyed containers ( [Codable] ) + let encoder = JSONEncoder() + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys + } + let json = try encoder.encode(value) + //the Data from the encoder is guaranteed to convert to String + let modelAsString = String(data: json, encoding: .utf8)! + return encode(modelAsString, key.stringValue) + } else { + throw(error) } - let json = try encoder.encode(value) - //the Data from the encoder is guaranteed to convert to String - let modelAsString = String(data: json, encoding: .utf8)! - return encode(modelAsString, key.stringValue) - } catch { + default: throw(error) } } @@ -162,16 +168,22 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) } catch { // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON - do { - let encoder = JSONEncoder() - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - encoder.outputFormatting = .sortedKeys + switch error as! EncodingError { + case .invalidValue(_, let context): + if context.debugDescription == "unkeyed encoding is not supported" { + // Support for keyed containers ( [Codable] ) + let encoder = JSONEncoder() + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys + } + let json = try encoder.encode(value) + //the Data from the encoder is guaranteed to convert to String + let modelAsString = String(data: json, encoding: .utf8)! + return encode(modelAsString, key.stringValue) + } else { + throw(error) } - let json = try encoder.encode(value) - //the Data from the encoder is guaranteed to convert to String - let modelAsString = String(data: json, encoding: .utf8)! - return encode(modelAsString, key.stringValue) - } catch { + default: throw(error) } } From 76f5adf944fa881cdf5ab43492cb8a60185d7fe9 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Tue, 14 Aug 2018 14:38:40 +0100 Subject: [PATCH 10/37] moved the extension to Swift.Data to follow the extension to Swift.String in the StandardLibrary.swift, added case .blob(let data): return String(data: data, encoding: .utf8) to String.fromDatabaseValue fixed tests --- GRDB.xcodeproj/project.pbxproj | 8 ---- GRDB/Core/Support/Foundation/Data.swift | 43 ------------------- .../StandardLibrary/StandardLibrary.swift | 36 ++++++++++++++-- GRDB/Record/FetchableRecord+Decodable.swift | 2 +- GRDBCipher.xcodeproj/project.pbxproj | 6 --- GRDBCustom.xcodeproj/project.pbxproj | 6 --- .../DatabaseValueConversionTests.swift | 26 +++++------ Tests/GRDBTests/FoundationDataTests.swift | 2 +- Tests/GRDBTests/FoundationNSDataTests.swift | 2 +- Tests/GRDBTests/FoundationNSStringTests.swift | 2 +- Tests/GRDBTests/FoundationNSURLTests.swift | 2 +- Tests/GRDBTests/FoundationURLTests.swift | 2 +- 12 files changed, 51 insertions(+), 86 deletions(-) delete mode 100644 GRDB/Core/Support/Foundation/Data.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 4ee666f455..8929f0dc28 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -371,8 +371,6 @@ 56873BF21F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; }; 5690C32A1D23E6D800E59934 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; }; 5690C33B1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; - 5690C3401D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; - 5690C3431D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 569178491CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; 569531091C9067DC00CF1A2B /* DatabaseQueueCrashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569530FB1C9067CC00CF1A2B /* DatabaseQueueCrashTests.swift */; }; 5695310A1C9067DC00CF1A2B /* DatabaseValueConvertibleCrashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569530FC1C9067CC00CF1A2B /* DatabaseValueConvertibleCrashTests.swift */; }; @@ -673,7 +671,6 @@ 56F3E7631E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; }; 56F3E7661E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; }; 56F3E7691E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; }; - 56F5ABD91D814330001F60CB /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 56F5ABDA1D814330001F60CB /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AAB81D107001006283EF /* NSData.swift */; }; 56F5ABDC1D814330001F60CB /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB0E1D10899D006283EF /* URL.swift */; }; 56F5ABDD1D814330001F60CB /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; }; @@ -956,7 +953,6 @@ 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fixits-1.2.swift"; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; - 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; 569530FB1C9067CC00CF1A2B /* DatabaseQueueCrashTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueCrashTests.swift; sourceTree = ""; }; 569530FC1C9067CC00CF1A2B /* DatabaseValueConvertibleCrashTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleCrashTests.swift; sourceTree = ""; }; @@ -1217,7 +1213,6 @@ 5605F14A1C672E4000235C62 /* Foundation */ = { isa = PBXGroup; children = ( - 5690C33F1D23E82A00E59934 /* Data.swift */, 5605F14C1C672E4000235C62 /* DatabaseDateComponents.swift */, 5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */, 5605F14F1C672E4000235C62 /* Date.swift */, @@ -2383,7 +2378,6 @@ 565490C61D5AE236005622CB /* Statement.swift in Sources */, 56CEB5071EAA2F4D00BFAF62 /* FTS4.swift in Sources */, 56CEB4F71EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, - 56F5ABD91D814330001F60CB /* Data.swift in Sources */, 565490D41D5AE252005622CB /* (null) in Sources */, 566475D21D981D5E00FF74B8 /* SQLFunctions.swift in Sources */, 565490B61D5AE236005622CB /* Configuration.swift in Sources */, @@ -2494,7 +2488,6 @@ 5659F4931EA8D964004A4992 /* ReadWriteBox.swift in Sources */, 5605F1941C6B1A8700235C62 /* QueryInterfaceQuery.swift in Sources */, 56B7F43B1BEB42D500E39BBF /* Migration.swift in Sources */, - 5690C3431D23E82A00E59934 /* Data.swift in Sources */, 5674A7001F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */, 5674A6E91F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift in Sources */, 566B91161FA4C3F50012D5B0 /* DatabaseCollation.swift in Sources */, @@ -2935,7 +2928,6 @@ 5605F1931C6B1A8700235C62 /* QueryInterfaceQuery.swift in Sources */, 566B912B1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 56A2388B1B9C75030082EB20 /* Statement.swift in Sources */, - 5690C3401D23E82A00E59934 /* Data.swift in Sources */, 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */, 56FC98781D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */, diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift deleted file mode 100644 index 3f2e4af679..0000000000 --- a/GRDB/Core/Support/Foundation/Data.swift +++ /dev/null @@ -1,43 +0,0 @@ -import Foundation -#if SWIFT_PACKAGE - import CSQLite -#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER - import SQLite3 -#endif - -/// Data is convertible to and from DatabaseValue. -extension Data : DatabaseValueConvertible, StatementColumnConvertible { - public init(sqliteStatement: SQLiteStatement, index: Int32) { - if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { - let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) - self.init(bytes: bytes, count: count) // copy bytes - } else { - self.init() - } - } - - /// Returns a value that can be stored in the database. - public var databaseValue: DatabaseValue { - return DatabaseValue(storage: .blob(self)) - } - - /// Returns a Data initialized from *dbValue*, if it contains - /// a Blob. - public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { - guard case .blob(let data) = dbValue.storage else { - // Check to see if data is String, if so then pass is through JSONSerialization - // to confirm if contains JSON, if so then this is a nested type stored as JSON, - // so return string as Data so that it can be decoded - if let valueIsString = dbValue.storage.value as? String, let dataUtf8 = valueIsString.data(using: .utf8) { - do { - try JSONSerialization.jsonObject(with: dataUtf8, options: []) - return dataUtf8 - } catch { - return nil - } - } - return nil - } - return data - } -} diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index cabe0a5c88..0701143338 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -4,6 +4,8 @@ import SQLite3 #endif +import Foundation + // MARK: - Value Types /// Bool adopts DatabaseValueConvertible and StatementColumnConvertible. @@ -456,10 +458,36 @@ extension String: DatabaseValueConvertible, StatementColumnConvertible { /// Returns a String initialized from *dbValue*, if possible. public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> String? { switch dbValue.storage { - case .string(let string): - return string - default: - return nil + case .blob(let data): return String(data: data, encoding: .utf8) + case .string(let string): return string + default: return nil + } + } +} + +/// Data is convertible to and from DatabaseValue. +extension Data : DatabaseValueConvertible, StatementColumnConvertible { + public init(sqliteStatement: SQLiteStatement, index: Int32) { + if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { + let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) + self.init(bytes: bytes, count: count) // copy bytes + } else { + self.init() + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return DatabaseValue(storage: .blob(self)) + } + + /// Returns a Data initialized from *dbValue*, if it contains + /// a Blob. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { + switch dbValue.storage { + case .blob(let data): return data + case .string(let string): return string.data(using: .utf8) + default: return nil } } } diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index b1f0e1d0d4..42ba769fb8 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -86,7 +86,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // because we're decoding a database value here (string, int, double, data, null) // if we find a nested Decodable type then pass the string to JSON decoder let value = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) - if let _ = value as? Codable { + if let _ = value as? Encodable { // Support for nested ( Codable ) return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) } else { diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index 81603aead1..98fcac3d85 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -500,8 +500,6 @@ 5690C3391D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C33C1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C33D1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; - 5690C3411D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; - 5690C3441D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 569178471CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; 569178481CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; 5691784A1CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; @@ -1057,7 +1055,6 @@ 568E1CB81CB03847008D97A6 /* GRDBCipher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GRDBCipher.h; path = SQLCipher/GRDBCipher.h; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; - 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSchemaCache.swift; sourceTree = ""; }; 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueSchemaCacheTests.swift; sourceTree = ""; }; @@ -1264,7 +1261,6 @@ 5605F14A1C672E4000235C62 /* Foundation */ = { isa = PBXGroup; children = ( - 5690C33F1D23E82A00E59934 /* Data.swift */, 5605F14C1C672E4000235C62 /* DatabaseDateComponents.swift */, 5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */, 5605F14F1C672E4000235C62 /* Date.swift */, @@ -2206,7 +2202,6 @@ 566B912C1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 560FC5351CB003810014AA8E /* Statement.swift in Sources */, 5653EB9320961FC000F46237 /* AssociationQuery.swift in Sources */, - 5690C3411D23E82A00E59934 /* Data.swift in Sources */, 5659F4891EA8D94E004A4992 /* Utils.swift in Sources */, 560FC5361CB003810014AA8E /* DatabaseMigrator.swift in Sources */, 560FC5371CB003810014AA8E /* DatabaseSchemaCache.swift in Sources */, @@ -2641,7 +2636,6 @@ 566B912F1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 56AFCA0A1CB1A8BB00F48B96 /* Migration.swift in Sources */, 5653EB9420961FC000F46237 /* AssociationQuery.swift in Sources */, - 5690C3441D23E82A00E59934 /* Data.swift in Sources */, 5659F48C1EA8D94E004A4992 /* Utils.swift in Sources */, 56AFCA0B1CB1A8BB00F48B96 /* Row.swift in Sources */, 56AFCA0C1CB1A8BB00F48B96 /* DatabaseSchemaCache.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 1880e46011..f8d91bce70 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -201,8 +201,6 @@ 5690C32D1D23E6D800E59934 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; }; 5690C33A1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C33E1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; - 5690C3421D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; - 5690C3451D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 5698AC061D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */; }; 5698AC0A1D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */; }; 5698AC391D9E5A590056AF8C /* FTS3Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC361D9E5A590056AF8C /* FTS3Pattern.swift */; }; @@ -721,7 +719,6 @@ 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fixits-1.2.swift"; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; - 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSchemaCache.swift; sourceTree = ""; }; 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueSchemaCacheTests.swift; sourceTree = ""; }; @@ -923,7 +920,6 @@ 5605F14A1C672E4000235C62 /* Foundation */ = { isa = PBXGroup; children = ( - 5690C33F1D23E82A00E59934 /* Data.swift */, 5605F14C1C672E4000235C62 /* DatabaseDateComponents.swift */, 5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */, 5605F14F1C672E4000235C62 /* Date.swift */, @@ -1881,7 +1877,6 @@ F3BA80321CFB28A4003DC1BA /* FetchedRecordsController.swift in Sources */, 5659F48D1EA8D94E004A4992 /* Utils.swift in Sources */, F3BA80231CFB288C003DC1BA /* NSString.swift in Sources */, - 5690C3451D23E82A00E59934 /* Data.swift in Sources */, 56FC987D1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, F3BA80111CFB2876003DC1BA /* DatabaseSchemaCache.swift in Sources */, F3BA80281CFB2891003DC1BA /* StandardLibrary.swift in Sources */, @@ -2154,7 +2149,6 @@ F3BA808E1CFB2E7A003DC1BA /* FetchedRecordsController.swift in Sources */, 5659F48A1EA8D94E004A4992 /* Utils.swift in Sources */, F3BA807F1CFB2E61003DC1BA /* NSString.swift in Sources */, - 5690C3421D23E82A00E59934 /* Data.swift in Sources */, 56FC987A1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, F3BA806D1CFB2E55003DC1BA /* DatabaseSchemaCache.swift in Sources */, F3BA80841CFB2E67003DC1BA /* StandardLibrary.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index d25b23bca6..78c178762d 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -75,7 +75,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0".data(using: .utf8)) return .rollback } @@ -94,7 +94,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0".data(using: .utf8)) return .rollback } @@ -113,7 +113,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0".data(using: .utf8)) return .rollback } @@ -132,7 +132,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0.0") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0.0".data(using: .utf8)) return .rollback } @@ -151,7 +151,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "3.0e+5") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "3.0e+5".data(using: .utf8)) return .rollback } @@ -170,7 +170,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -188,7 +188,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback @@ -385,7 +385,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -403,7 +403,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback @@ -509,7 +509,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "3.0e+5") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "3.0e+5".data(using: .utf8)) return .rollback } @@ -527,7 +527,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback @@ -698,7 +698,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -716,7 +716,7 @@ class DatabaseValueConversionTests : GRDBTestCase { XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) + XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback diff --git a/Tests/GRDBTests/FoundationDataTests.swift b/Tests/GRDBTests/FoundationDataTests.swift index eb8551bb35..fc798ed0b3 100644 --- a/Tests/GRDBTests/FoundationDataTests.swift +++ b/Tests/GRDBTests/FoundationDataTests.swift @@ -48,6 +48,6 @@ class FoundationDataTests: GRDBTestCase { XCTAssertNil(Data.fromDatabaseValue(databaseValue_Null)) XCTAssertNil(Data.fromDatabaseValue(databaseValue_Int64)) XCTAssertNil(Data.fromDatabaseValue(databaseValue_Double)) - XCTAssertNil(Data.fromDatabaseValue(databaseValue_String)) + XCTAssertEqual(Data.fromDatabaseValue(databaseValue_String), "foo".data(using: .utf8)) } } diff --git a/Tests/GRDBTests/FoundationNSDataTests.swift b/Tests/GRDBTests/FoundationNSDataTests.swift index 9ecd8b492e..6ae612ec0e 100644 --- a/Tests/GRDBTests/FoundationNSDataTests.swift +++ b/Tests/GRDBTests/FoundationNSDataTests.swift @@ -48,6 +48,6 @@ class FoundationNSDataTests: GRDBTestCase { XCTAssertNil(NSData.fromDatabaseValue(databaseValue_Null)) XCTAssertNil(NSData.fromDatabaseValue(databaseValue_Int64)) XCTAssertNil(NSData.fromDatabaseValue(databaseValue_Double)) - XCTAssertNil(NSData.fromDatabaseValue(databaseValue_String)) + XCTAssertEqual(NSData.fromDatabaseValue(databaseValue_String)! as Data, "foo".data(using: .utf8)) } } diff --git a/Tests/GRDBTests/FoundationNSStringTests.swift b/Tests/GRDBTests/FoundationNSStringTests.swift index 083f7bef99..258ca6321a 100644 --- a/Tests/GRDBTests/FoundationNSStringTests.swift +++ b/Tests/GRDBTests/FoundationNSStringTests.swift @@ -55,7 +55,7 @@ class FoundationNSStringTests: GRDBTestCase { XCTAssertNil(NSString.fromDatabaseValue(databaseValue_Null)) XCTAssertNil(NSString.fromDatabaseValue(databaseValue_Int64)) XCTAssertNil(NSString.fromDatabaseValue(databaseValue_Double)) - XCTAssertNil(NSString.fromDatabaseValue(databaseValue_Blob)) + XCTAssertEqual(NSString.fromDatabaseValue(databaseValue_Blob), "bar") } } diff --git a/Tests/GRDBTests/FoundationNSURLTests.swift b/Tests/GRDBTests/FoundationNSURLTests.swift index 7588c0b32a..404a489351 100644 --- a/Tests/GRDBTests/FoundationNSURLTests.swift +++ b/Tests/GRDBTests/FoundationNSURLTests.swift @@ -48,7 +48,7 @@ class FoundationNSURLTests: GRDBTestCase { XCTAssertNil(NSURL.fromDatabaseValue(databaseValue_Null)) XCTAssertNil(NSURL.fromDatabaseValue(databaseValue_Int64)) XCTAssertNil(NSURL.fromDatabaseValue(databaseValue_Double)) - XCTAssertNil(NSURL.fromDatabaseValue(databaseValue_Blob)) + XCTAssertEqual(NSURL.fromDatabaseValue(databaseValue_Blob)!.absoluteString, "bar") } } diff --git a/Tests/GRDBTests/FoundationURLTests.swift b/Tests/GRDBTests/FoundationURLTests.swift index a476f74d57..1e2a96f2da 100644 --- a/Tests/GRDBTests/FoundationURLTests.swift +++ b/Tests/GRDBTests/FoundationURLTests.swift @@ -48,7 +48,7 @@ class FoundationURLTests: GRDBTestCase { XCTAssertNil(URL.fromDatabaseValue(databaseValue_Null)) XCTAssertNil(URL.fromDatabaseValue(databaseValue_Int64)) XCTAssertNil(URL.fromDatabaseValue(databaseValue_Double)) - XCTAssertNil(URL.fromDatabaseValue(databaseValue_Blob)) + XCTAssertEqual(URL.fromDatabaseValue(databaseValue_Blob)!.absoluteString, "bar") } } From c52b59f1bc1af45c7a560a12304962d7775fc5f0 Mon Sep 17 00:00:00 2001 From: Angus Muller Date: Tue, 14 Aug 2018 16:01:05 +0100 Subject: [PATCH 11/37] Switching to JSON encoding for nested containers. --- GRDB/Record/PersistableRecord+Encodable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index e7f4005b05..aa647ea30e 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -268,7 +268,7 @@ class ThrowingKeyedContainer: KeyedEncodingContainerProtocol func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw errorMessage } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer where NestedKey : CodingKey { - fatalError("Not implemented") + return KeyedEncodingContainer(ThrowingKeyedContainer(error: errorMessage)) } func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { fatalError("Not implemented") @@ -309,7 +309,7 @@ class ThrowingUnkeyedContainer: UnkeyedEncodingContainer { func encodeNil() throws { throw errorMessage } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - fatalError("Not implemented") + return KeyedEncodingContainer(ThrowingKeyedContainer(error: errorMessage)) } From 05fda18a56e7b2e7eaf800eefb7e87c7a5ba570f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 09:55:45 +0200 Subject: [PATCH 12/37] Introduce RowSingleValueDecoder This allows complex decodable types to fail decoding from a row column, so that we switch to JSON decoding. --- GRDB/Record/FetchableRecord+Decodable.swift | 107 +++++++++++--------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index ed45f804e9..b244cf1bb0 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -84,27 +84,19 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer return nil } else { do { - // This decoding will fail for types that need a keyed container, - // because we're decoding a database value here (string, int, double, data, null) - // if we find a nested Decodable type then pass the string to JSON decoder - let value = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) - if let _ = value as? Encodable { - // Support for nested ( Codable ) - return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) + // This decoding will fail for types that decode from keyed + // or unkeyed containers, because we're decoding a single + // value here (string, int, double, data, null). If such an + // error happens, we'll switch to JSON decoding. + return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) + } catch let error as DecodingError { + if case .typeMismatch(_, let context) = error, + context.debugDescription == "keyed decoding is not supported" || context.debugDescription == "unkeyed decoding is not supported", + let data = row.dataNoCopy(atIndex: index) + { + return try JSONDecoder().decode(type.self, from: data) } else { - return value - } - } catch { - switch error as! DecodingError { - case .typeMismatch(_, let context): - if context.debugDescription == "unkeyed decoding is not supported" { - // Support for nested keyed containers ( [Codable] ) - return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) - } else { - throw(error) - } - default: - throw(error) + throw error } } } @@ -146,27 +138,19 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer return type.decode(from: row, atUncheckedIndex: index) as! T } else { do { - // This decoding will fail for types that need a keyed container, - // because we're decoding a database value here (string, int, double, data, null) - // if we find a nested Decodable type then pass the string to JSON decoder - let value = try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) - if let _ = value as? Codable { - // Support for nested ( Codable ) - return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) + // This decoding will fail for types that decode from keyed + // or unkeyed containers, because we're decoding a single + // value here (string, int, double, data, null). If such an + // error happens, we'll switch to JSON decoding. + return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) + } catch let error as DecodingError { + if case .typeMismatch(_, let context) = error, + context.debugDescription == "keyed decoding is not supported" || context.debugDescription == "unkeyed decoding is not supported", + let data = row.dataNoCopy(atIndex: index) + { + return try JSONDecoder().decode(type.self, from: data) } else { - return value - } - } catch { - switch error as! DecodingError { - case .typeMismatch(_, let context): - if context.debugDescription == "unkeyed decoding is not supported" { - // Support for nested keyed containers ( [Codable] ) - return try JSONDecoder().decode(type.self, from: row.dataNoCopy(named: key.stringValue)!) - } else { - throw(error) - } - default: - throw(error) + throw error } } } @@ -279,9 +263,10 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. + // TODO: switch to more efficient decoding apis return type.decode(from: row[column.stringValue], conversionContext: ValueConversionContext(row).atColumn(column.stringValue)) as! T } else { - return try T(from: RowDecoder(row: row, codingPath: [column])) + return try T(from: RowSingleValueDecoder(row: row, codingPath: [column])) } } } @@ -309,13 +294,39 @@ private struct RowDecoder: Decoder { } func singleValueContainer() throws -> SingleValueDecodingContainer { - // Asked for a value type: column name required - guard let codingKey = codingPath.last else { - throw DecodingError.typeMismatch( - RowDecoder.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "single value decoding requires a coding key")) - } - return RowSingleValueDecodingContainer(row: row, codingPath: codingPath, column: codingKey) + throw DecodingError.typeMismatch( + RowDecoder.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "single value decoding is not supported")) + } +} + +private struct RowSingleValueDecoder: Decoder { + let row: Row + + init(row: Row, codingPath: [CodingKey]) { + assert(!codingPath.isEmpty, "coding key required") + self.row = row + self.codingPath = codingPath + } + + // Decoder + let codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey : Any] { return [:] } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + throw DecodingError.typeMismatch( + UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "keyed decoding is not supported")) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw DecodingError.typeMismatch( + UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + return RowSingleValueDecodingContainer(row: row, codingPath: codingPath, column: codingPath.last!) } } From 275098084d583e66a36f883f216273c16e33ec9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 10:12:28 +0200 Subject: [PATCH 13/37] Introduce JSONDecodingRequiredError We used to check for a standard DecodingError.typeMismatch error with specific message before switching to JSON decoding. There was a small risk that such an error would not be generated by GRDB. In this case, we'd erroneously switch to JSON decoding instead of throwing the error. So let's recognize the specific need for JSON decoding with a specific error instead: JSONDecodingRequiredError. --- GRDB/Record/FetchableRecord+Decodable.swift | 32 +++++++-------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index b244cf1bb0..f6fbcef203 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -89,11 +89,8 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) - } catch let error as DecodingError { - if case .typeMismatch(_, let context) = error, - context.debugDescription == "keyed decoding is not supported" || context.debugDescription == "unkeyed decoding is not supported", - let data = row.dataNoCopy(atIndex: index) - { + } catch let error as JSONDecodingRequiredError { + if let data = row.dataNoCopy(atIndex: index) { return try JSONDecoder().decode(type.self, from: data) } else { throw error @@ -143,11 +140,8 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) - } catch let error as DecodingError { - if case .typeMismatch(_, let context) = error, - context.debugDescription == "keyed decoding is not supported" || context.debugDescription == "unkeyed decoding is not supported", - let data = row.dataNoCopy(atIndex: index) - { + } catch let error as JSONDecodingRequiredError { + if let data = row.dataNoCopy(atIndex: index) { return try JSONDecoder().decode(type.self, from: data) } else { throw error @@ -288,15 +282,11 @@ private struct RowDecoder: Decoder { } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw DecodingError.typeMismatch( - UnkeyedDecodingContainer.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + throw JSONDecodingRequiredError() } func singleValueContainer() throws -> SingleValueDecodingContainer { - throw DecodingError.typeMismatch( - RowDecoder.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "single value decoding is not supported")) + throw JSONDecodingRequiredError() } } @@ -314,15 +304,11 @@ private struct RowSingleValueDecoder: Decoder { var userInfo: [CodingUserInfoKey : Any] { return [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - throw DecodingError.typeMismatch( - UnkeyedDecodingContainer.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "keyed decoding is not supported")) + throw JSONDecodingRequiredError() } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw DecodingError.typeMismatch( - UnkeyedDecodingContainer.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + throw JSONDecodingRequiredError() } func singleValueContainer() throws -> SingleValueDecodingContainer { @@ -330,6 +316,8 @@ private struct RowSingleValueDecoder: Decoder { } } +private struct JSONDecodingRequiredError: Error { } + extension FetchableRecord where Self: Decodable { /// Initializes a record from `row`. public init(row: Row) { From aa8bc0fae325d9b6243b10dd0f522e1cc24efd42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 10:24:28 +0200 Subject: [PATCH 14/37] Don't let JSONDecodingRequiredError go JSON decoding: when row doesn't contain any Data, we don't want to expose a JSONDecodingRequiredError error to the user. We switch to a detailed conversion error instead (see #384). --- GRDB/Record/FetchableRecord+Decodable.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index f6fbcef203..135c219ebc 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -89,12 +89,11 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) - } catch let error as JSONDecodingRequiredError { - if let data = row.dataNoCopy(atIndex: index) { - return try JSONDecoder().decode(type.self, from: data) - } else { - throw error + } catch is JSONDecodingRequiredError { + guard let data = row.dataNoCopy(atIndex: index) else { + fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) } + return try JSONDecoder().decode(type.self, from: data) } } } @@ -140,12 +139,11 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) - } catch let error as JSONDecodingRequiredError { - if let data = row.dataNoCopy(atIndex: index) { - return try JSONDecoder().decode(type.self, from: data) - } else { - throw error + } catch is JSONDecodingRequiredError { + guard let data = row.dataNoCopy(atIndex: index) else { + fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) } + return try JSONDecoder().decode(type.self, from: data) } } } From 942b32e5d58c531e0b804b7052a9543916e454bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 10:40:14 +0200 Subject: [PATCH 15/37] RowSingleValueDecodingContainer performance improvements We switch from column indexing to integer indexing, which is far more efficient. And we can use the improved value decoding methods from #384. --- GRDB/Record/FetchableRecord+Decodable.swift | 54 +++++++++++---------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 135c219ebc..fb76347fe5 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -88,7 +88,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // or unkeyed containers, because we're decoding a single // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. - return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) + return try T(from: RowSingleValueDecoder(row: row, columnIndex: index, codingPath: codingPath + [key])) } catch is JSONDecodingRequiredError { guard let data = row.dataNoCopy(atIndex: index) else { fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) @@ -138,7 +138,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // or unkeyed containers, because we're decoding a single // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. - return try T(from: RowSingleValueDecoder(row: row, codingPath: codingPath + [key])) + return try T(from: RowSingleValueDecoder(row: row, columnIndex: index, codingPath: codingPath + [key])) } catch is JSONDecodingRequiredError { guard let data = row.dataNoCopy(atIndex: index) else { fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) @@ -216,12 +216,13 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { let row: Row var codingPath: [CodingKey] let column: CodingKey + let columnIndex: Int /// Decodes a null value. /// /// - returns: Whether the encountered value was null. func decodeNil() -> Bool { - return row[column.stringValue] == nil + return row.hasNull(atIndex: columnIndex) } /// Decodes a single value of the given type. @@ -230,20 +231,20 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { /// - returns: A value of the requested type. /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. - func decode(_ type: Bool.Type) throws -> Bool { return row[column.stringValue] } - func decode(_ type: Int.Type) throws -> Int { return row[column.stringValue] } - func decode(_ type: Int8.Type) throws -> Int8 { return row[column.stringValue] } - func decode(_ type: Int16.Type) throws -> Int16 { return row[column.stringValue] } - func decode(_ type: Int32.Type) throws -> Int32 { return row[column.stringValue] } - func decode(_ type: Int64.Type) throws -> Int64 { return row[column.stringValue] } - func decode(_ type: UInt.Type) throws -> UInt { return row[column.stringValue] } - func decode(_ type: UInt8.Type) throws -> UInt8 { return row[column.stringValue] } - func decode(_ type: UInt16.Type) throws -> UInt16 { return row[column.stringValue] } - func decode(_ type: UInt32.Type) throws -> UInt32 { return row[column.stringValue] } - func decode(_ type: UInt64.Type) throws -> UInt64 { return row[column.stringValue] } - func decode(_ type: Float.Type) throws -> Float { return row[column.stringValue] } - func decode(_ type: Double.Type) throws -> Double { return row[column.stringValue] } - func decode(_ type: String.Type) throws -> String { return row[column.stringValue] } + func decode(_ type: Bool.Type) throws -> Bool { return row[columnIndex] } + func decode(_ type: Int.Type) throws -> Int { return row[columnIndex] } + func decode(_ type: Int8.Type) throws -> Int8 { return row[columnIndex] } + func decode(_ type: Int16.Type) throws -> Int16 { return row[columnIndex] } + func decode(_ type: Int32.Type) throws -> Int32 { return row[columnIndex] } + func decode(_ type: Int64.Type) throws -> Int64 { return row[columnIndex] } + func decode(_ type: UInt.Type) throws -> UInt { return row[columnIndex] } + func decode(_ type: UInt8.Type) throws -> UInt8 { return row[columnIndex] } + func decode(_ type: UInt16.Type) throws -> UInt16 { return row[columnIndex] } + func decode(_ type: UInt32.Type) throws -> UInt32 { return row[columnIndex] } + func decode(_ type: UInt64.Type) throws -> UInt64 { return row[columnIndex] } + func decode(_ type: Float.Type) throws -> Float { return row[columnIndex] } + func decode(_ type: Double.Type) throws -> Double { return row[columnIndex] } + func decode(_ type: String.Type) throws -> String { return row[columnIndex] } /// Decodes a single value of the given type. /// @@ -252,13 +253,14 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { /// - throws: `DecodingError.typeMismatch` if the encountered encoded value cannot be converted to the requested type. /// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null. func decode(_ type: T.Type) throws -> T where T : Decodable { - if let type = T.self as? DatabaseValueConvertible.Type { - // Prefer DatabaseValueConvertible decoding over Decodable. - // This allows decoding Date from String, or DatabaseValue from NULL. - // TODO: switch to more efficient decoding apis - return type.decode(from: row[column.stringValue], conversionContext: ValueConversionContext(row).atColumn(column.stringValue)) as! T + // Prefer DatabaseValueConvertible decoding over Decodable. + // This allows decoding Date from String, or DatabaseValue from NULL. + if let type = T.self as? (DatabaseValueConvertible & StatementColumnConvertible).Type { + return type.fastDecode(from: row, atUncheckedIndex: columnIndex) as! T + } else if let type = T.self as? DatabaseValueConvertible.Type { + return type.decode(from: row, atUncheckedIndex: columnIndex) as! T } else { - return try T(from: RowSingleValueDecoder(row: row, codingPath: [column])) + return try T(from: RowSingleValueDecoder(row: row, columnIndex: columnIndex, codingPath: [column])) } } } @@ -290,10 +292,12 @@ private struct RowDecoder: Decoder { private struct RowSingleValueDecoder: Decoder { let row: Row + let columnIndex: Int - init(row: Row, codingPath: [CodingKey]) { + init(row: Row, columnIndex: Int, codingPath: [CodingKey]) { assert(!codingPath.isEmpty, "coding key required") self.row = row + self.columnIndex = columnIndex self.codingPath = codingPath } @@ -310,7 +314,7 @@ private struct RowSingleValueDecoder: Decoder { } func singleValueContainer() throws -> SingleValueDecodingContainer { - return RowSingleValueDecodingContainer(row: row, codingPath: codingPath, column: codingPath.last!) + return RowSingleValueDecodingContainer(row: row, codingPath: codingPath, column: codingPath.last!, columnIndex: columnIndex) } } From 0cdc7d81d43044bc43c4799e4e87df9fe1a2eab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 11:10:11 +0200 Subject: [PATCH 16/37] Just some cleanup, nothing new --- GRDB/Record/FetchableRecord+Decodable.swift | 43 +++++++-------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index fb76347fe5..f66a852dfd 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -2,13 +2,12 @@ import Foundation private struct RowKeyedDecodingContainer: KeyedDecodingContainerProtocol { let decoder: RowDecoder - + var codingPath: [CodingKey] { return decoder.codingPath } + init(decoder: RowDecoder) { self.decoder = decoder } - var codingPath: [CodingKey] { return decoder.codingPath } - /// All the keys the `Decoder` has for this container. /// /// Different keyed containers from the same `Decoder` may return different keys here; it is possible to encode with multiple key types which are not convertible to one another. This should report all keys present which are convertible to the requested type. @@ -213,11 +212,10 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { - let row: Row + var row: Row + var columnIndex: Int var codingPath: [CodingKey] - let column: CodingKey - let columnIndex: Int - + /// Decodes a null value. /// /// - returns: Whether the encountered value was null. @@ -260,21 +258,14 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { } else if let type = T.self as? DatabaseValueConvertible.Type { return type.decode(from: row, atUncheckedIndex: columnIndex) as! T } else { - return try T(from: RowSingleValueDecoder(row: row, columnIndex: columnIndex, codingPath: [column])) + return try T(from: RowSingleValueDecoder(row: row, columnIndex: columnIndex, codingPath: codingPath)) } } } private struct RowDecoder: Decoder { - let row: Row - - init(row: Row, codingPath: [CodingKey]) { - self.row = row - self.codingPath = codingPath - } - - // Decoder - let codingPath: [CodingKey] + var row: Row + var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey : Any] { return [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { @@ -291,18 +282,9 @@ private struct RowDecoder: Decoder { } private struct RowSingleValueDecoder: Decoder { - let row: Row - let columnIndex: Int - - init(row: Row, columnIndex: Int, codingPath: [CodingKey]) { - assert(!codingPath.isEmpty, "coding key required") - self.row = row - self.columnIndex = columnIndex - self.codingPath = codingPath - } - - // Decoder - let codingPath: [CodingKey] + var row: Row + var columnIndex: Int + var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey : Any] { return [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { @@ -314,10 +296,11 @@ private struct RowSingleValueDecoder: Decoder { } func singleValueContainer() throws -> SingleValueDecodingContainer { - return RowSingleValueDecodingContainer(row: row, codingPath: codingPath, column: codingPath.last!, columnIndex: columnIndex) + return RowSingleValueDecodingContainer(row: row, columnIndex: columnIndex, codingPath: codingPath) } } +/// The error that triggers JSON decoding private struct JSONDecodingRequiredError: Error { } extension FetchableRecord where Self: Decodable { From 5f41c57c454fc8872a0ce6189fb88691ab61ef04 Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Wed, 15 Aug 2018 10:21:55 +0100 Subject: [PATCH 17/37] amended the documentation and tests to include Dictionary support to Codable types --- README.md | 6 +++--- .../FetchableRecordDecodableTests.swift | 19 +++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 47836e6202..7e555a5cff 100644 --- a/README.md +++ b/README.md @@ -2567,19 +2567,19 @@ struct Link : PersistableRecord { ## Codable Records -[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4, GRDB supports all Codable conforming types with the exception of Dictionaries. +[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4, GRDB supports all Codable conforming types GRDB provides default implementations for [`FetchableRecord.init(row:)`](#fetchablerecord-protocol) and [`PersistableRecord.encode(to:)`](#persistablerecord-protocol) for record types that also adopt an archival protocol (`Codable`, `Encodable` or `Decodable`). When all their properties are themselves codable, Swift generates the archiving methods, and you don't need to write them down: ```swift -// Declare a Codable struct or class, nested Codable objects as well as Sets, Arrays, and Optionals are supported with the exception of Dictionaries +// Declare a Codable struct or class, nested Codable objects as well as Sets, Arrays, and Optionals are supported struct Player: Codable { let name: String let score: Int let scores: [Int] let lastMedal: PlayerMedal let medals: [PlayerMedal] - //let timeline: [String: PlayerMedal] // <- Conforms to Codable but is not supported by GRDB + //let timeline: [String: PlayerMedal] } // A simple Codable that will be nested in a parent Codable diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index ddb2cf3011..ac233da93d 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -506,7 +506,7 @@ extension FetchableRecordDecodableTests { let scores: [Int] let lastMedal: PlayerMedal let medals: [PlayerMedal] - //let timeline: [String: PlayerMedal] // <- Conforms to Codable but is not supported by GRDB + let timeline: [String: PlayerMedal] } // A simple Codable that will be nested in a parent Codable @@ -523,28 +523,35 @@ extension FetchableRecordDecodableTests { t.column("scores", .integer) t.column("lastMedal", .text) t.column("medals", .text) - //t.column("timeline", .text) + t.column("timeline", .text) } let medal1 = PlayerMedal(name: "First", type: "Gold") let medal2 = PlayerMedal(name: "Second", type: "Silver") - //let timeline = ["Local Contest":medal1, "National Contest":medal2] - let value = Player(name: "PlayerName", score: 10, scores: [1,2,3,4,5], lastMedal: medal1, medals: [medal1, medal2]) + let timeline = ["Local Contest": medal1, "National Contest": medal2] + let value = Player(name: "PlayerName", score: 10, scores: [1,2,3,4,5], lastMedal: medal1, medals: [medal1, medal2], timeline: timeline) try value.insert(db) let parentModel = try Player.fetchAll(db) - guard let arrayOfNestedModel = parentModel.first?.medals, let firstNestedModelInArray = arrayOfNestedModel.first, let secondNestedModelInArray = arrayOfNestedModel.last else { + guard let first = parentModel.first, let firstNestedModelInArray = first.medals.first, let secondNestedModelInArray = first.medals.last else { XCTFail() return } // Check there are two models in array - XCTAssertTrue(arrayOfNestedModel.count == 2) + XCTAssertTrue(first.medals.count == 2) // Check the nested model contains the expected values of first and last name XCTAssertEqual(firstNestedModelInArray.name, "First") XCTAssertEqual(secondNestedModelInArray.name, "Second") + + XCTAssertEqual(first.name, "PlayerName") + XCTAssertEqual(first.score, 10) + XCTAssertEqual(first.scores, [1,2,3,4,5]) + XCTAssertEqual(first.lastMedal.name, medal1.name) + XCTAssertEqual(first.timeline["Local Contest"]?.name, medal1.name) + XCTAssertEqual(first.timeline["National Contest"]?.name, medal2.name) } } From 1c66ad4aaddb42e34ec5f35291f955a3786a65c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 12:05:16 +0200 Subject: [PATCH 18/37] Restore GRDB/Core/Support/Foundation/Data.swift because Data belongs to Foundation, not to the Standard Library. Remove `import Foundation` from StandardLibrary.swift --- GRDB.xcodeproj/project.pbxproj | 8 ++++ GRDB/Core/Support/Foundation/Data.swift | 38 ++++++++++++++++++ .../StandardLibrary/StandardLibrary.swift | 40 ++++--------------- GRDBCipher.xcodeproj/project.pbxproj | 6 +++ GRDBCustom.xcodeproj/project.pbxproj | 6 +++ 5 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 GRDB/Core/Support/Foundation/Data.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index f93d945d07..22e81d0484 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -380,6 +380,8 @@ 5690AFDC212058CB001530EA /* FetchRecordCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */; }; 5690C32A1D23E6D800E59934 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; }; 5690C33B1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; + 5690C3401D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; + 5690C3431D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 569178491CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; 569531091C9067DC00CF1A2B /* DatabaseQueueCrashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569530FB1C9067CC00CF1A2B /* DatabaseQueueCrashTests.swift */; }; 5695310A1C9067DC00CF1A2B /* DatabaseValueConvertibleCrashTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569530FC1C9067CC00CF1A2B /* DatabaseValueConvertibleCrashTests.swift */; }; @@ -680,6 +682,7 @@ 56F3E7631E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; }; 56F3E7661E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; }; 56F3E7691E67F8C100BF0F01 /* Fixits-0.101.1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */; }; + 56F5ABD91D814330001F60CB /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 56F5ABDA1D814330001F60CB /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AAB81D107001006283EF /* NSData.swift */; }; 56F5ABDC1D814330001F60CB /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5657AB0E1D10899D006283EF /* URL.swift */; }; 56F5ABDD1D814330001F60CB /* UUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A8C22F1D1914540096E9D4 /* UUID.swift */; }; @@ -966,6 +969,7 @@ 5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRecordCodableTests.swift; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; + 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; 569530FB1C9067CC00CF1A2B /* DatabaseQueueCrashTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueCrashTests.swift; sourceTree = ""; }; 569530FC1C9067CC00CF1A2B /* DatabaseValueConvertibleCrashTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleCrashTests.swift; sourceTree = ""; }; @@ -1226,6 +1230,7 @@ 5605F14A1C672E4000235C62 /* Foundation */ = { isa = PBXGroup; children = ( + 5690C33F1D23E82A00E59934 /* Data.swift */, 5605F14C1C672E4000235C62 /* DatabaseDateComponents.swift */, 5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */, 5605F14F1C672E4000235C62 /* Date.swift */, @@ -2399,6 +2404,7 @@ 565490C61D5AE236005622CB /* Statement.swift in Sources */, 56CEB5071EAA2F4D00BFAF62 /* FTS4.swift in Sources */, 56CEB4F71EAA2EFA00BFAF62 /* FetchableRecord.swift in Sources */, + 56F5ABD91D814330001F60CB /* Data.swift in Sources */, 565490D41D5AE252005622CB /* (null) in Sources */, 566475D21D981D5E00FF74B8 /* SQLFunctions.swift in Sources */, 565490B61D5AE236005622CB /* Configuration.swift in Sources */, @@ -2510,6 +2516,7 @@ 5659F4931EA8D964004A4992 /* ReadWriteBox.swift in Sources */, 5605F1941C6B1A8700235C62 /* QueryInterfaceQuery.swift in Sources */, 56B7F43B1BEB42D500E39BBF /* Migration.swift in Sources */, + 5690C3431D23E82A00E59934 /* Data.swift in Sources */, 5674A7001F307F600095F066 /* FetchableRecord+Decodable.swift in Sources */, 5674A6E91F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift in Sources */, 566B91161FA4C3F50012D5B0 /* DatabaseCollation.swift in Sources */, @@ -2953,6 +2960,7 @@ 5605F1931C6B1A8700235C62 /* QueryInterfaceQuery.swift in Sources */, 566B912B1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 56A2388B1B9C75030082EB20 /* Statement.swift in Sources */, + 5690C3401D23E82A00E59934 /* Data.swift in Sources */, 5659F4881EA8D94E004A4992 /* Utils.swift in Sources */, 56FC98781D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, 56A238931B9C750B0082EB20 /* DatabaseMigrator.swift in Sources */, diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift new file mode 100644 index 0000000000..80a49dfcdb --- /dev/null +++ b/GRDB/Core/Support/Foundation/Data.swift @@ -0,0 +1,38 @@ +import Foundation +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + +/// Data is convertible to and from DatabaseValue. +extension Data : DatabaseValueConvertible, StatementColumnConvertible { + public init(sqliteStatement: SQLiteStatement, index: Int32) { + if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { + let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) + self.init(bytes: bytes, count: count) // copy bytes + } else { + self.init() + } + } + + /// Returns a value that can be stored in the database. + public var databaseValue: DatabaseValue { + return DatabaseValue(storage: .blob(self)) + } + + /// Returns a Data initialized from *dbValue*, if it contains + /// a Blob. + public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { + switch dbValue.storage { + case .blob(let data): + return data + case .string(let string): + // Implicit conversion from string to blob, just as SQLite does + // See https://www.sqlite.org/c3ref/column_blob.html + return string.data(using: .utf8) + default: + return nil + } + } +} diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index 0701143338..4397ffe7eb 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -4,8 +4,6 @@ import SQLite3 #endif -import Foundation - // MARK: - Value Types /// Bool adopts DatabaseValueConvertible and StatementColumnConvertible. @@ -458,36 +456,14 @@ extension String: DatabaseValueConvertible, StatementColumnConvertible { /// Returns a String initialized from *dbValue*, if possible. public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> String? { switch dbValue.storage { - case .blob(let data): return String(data: data, encoding: .utf8) - case .string(let string): return string - default: return nil - } - } -} - -/// Data is convertible to and from DatabaseValue. -extension Data : DatabaseValueConvertible, StatementColumnConvertible { - public init(sqliteStatement: SQLiteStatement, index: Int32) { - if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { - let count = Int(sqlite3_column_bytes(sqliteStatement, Int32(index))) - self.init(bytes: bytes, count: count) // copy bytes - } else { - self.init() - } - } - - /// Returns a value that can be stored in the database. - public var databaseValue: DatabaseValue { - return DatabaseValue(storage: .blob(self)) - } - - /// Returns a Data initialized from *dbValue*, if it contains - /// a Blob. - public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? { - switch dbValue.storage { - case .blob(let data): return data - case .string(let string): return string.data(using: .utf8) - default: return nil + case .blob(let data): + // Implicit conversion from blob to string, just as SQLite does + // See https://www.sqlite.org/c3ref/column_blob.html + return String(data: data, encoding: .utf8) + case .string(let string): + return string + default: + return nil } } } diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index c82e02a9aa..04f6e7ab9d 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -506,6 +506,8 @@ 5690C3391D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C33C1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C33D1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; + 5690C3411D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; + 5690C3441D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 569178471CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; 569178481CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; 5691784A1CED9B6000E179EA /* DatabaseQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */; }; @@ -1063,6 +1065,7 @@ 568E1CB81CB03847008D97A6 /* GRDBCipher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GRDBCipher.h; path = SQLCipher/GRDBCipher.h; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; + 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSchemaCache.swift; sourceTree = ""; }; 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueSchemaCacheTests.swift; sourceTree = ""; }; @@ -1269,6 +1272,7 @@ 5605F14A1C672E4000235C62 /* Foundation */ = { isa = PBXGroup; children = ( + 5690C33F1D23E82A00E59934 /* Data.swift */, 5605F14C1C672E4000235C62 /* DatabaseDateComponents.swift */, 5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */, 5605F14F1C672E4000235C62 /* Date.swift */, @@ -2213,6 +2217,7 @@ 566B912C1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 560FC5351CB003810014AA8E /* Statement.swift in Sources */, 5653EB9320961FC000F46237 /* AssociationQuery.swift in Sources */, + 5690C3411D23E82A00E59934 /* Data.swift in Sources */, 5659F4891EA8D94E004A4992 /* Utils.swift in Sources */, 560FC5361CB003810014AA8E /* DatabaseMigrator.swift in Sources */, 560FC5371CB003810014AA8E /* DatabaseSchemaCache.swift in Sources */, @@ -2650,6 +2655,7 @@ 566B912F1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, 56AFCA0A1CB1A8BB00F48B96 /* Migration.swift in Sources */, 5653EB9420961FC000F46237 /* AssociationQuery.swift in Sources */, + 5690C3441D23E82A00E59934 /* Data.swift in Sources */, 5659F48C1EA8D94E004A4992 /* Utils.swift in Sources */, 56AFCA0B1CB1A8BB00F48B96 /* Row.swift in Sources */, 56AFCA0C1CB1A8BB00F48B96 /* DatabaseSchemaCache.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 35ebb4b86c..998fa7c85b 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -205,6 +205,8 @@ 5690C32D1D23E6D800E59934 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; }; 5690C33A1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C33E1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; + 5690C3421D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; + 5690C3451D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; 5698AC061D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */; }; 5698AC0A1D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC021D9B9FCF0056AF8C /* QueryInterfaceExtensibilityTests.swift */; }; 5698AC391D9E5A590056AF8C /* FTS3Pattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5698AC361D9E5A590056AF8C /* FTS3Pattern.swift */; }; @@ -725,6 +727,7 @@ 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fixits-1.2.swift"; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; + 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 569178451CED9B6000E179EA /* DatabaseQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueTests.swift; sourceTree = ""; }; 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseSchemaCache.swift; sourceTree = ""; }; 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseQueueSchemaCacheTests.swift; sourceTree = ""; }; @@ -926,6 +929,7 @@ 5605F14A1C672E4000235C62 /* Foundation */ = { isa = PBXGroup; children = ( + 5690C33F1D23E82A00E59934 /* Data.swift */, 5605F14C1C672E4000235C62 /* DatabaseDateComponents.swift */, 5674A7021F307FCD0095F066 /* DatabaseValueConvertible+ReferenceConvertible.swift */, 5605F14F1C672E4000235C62 /* Date.swift */, @@ -1886,6 +1890,7 @@ F3BA80321CFB28A4003DC1BA /* FetchedRecordsController.swift in Sources */, 5659F48D1EA8D94E004A4992 /* Utils.swift in Sources */, F3BA80231CFB288C003DC1BA /* NSString.swift in Sources */, + 5690C3451D23E82A00E59934 /* Data.swift in Sources */, 56FC987D1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, F3BA80111CFB2876003DC1BA /* DatabaseSchemaCache.swift in Sources */, F3BA80281CFB2891003DC1BA /* StandardLibrary.swift in Sources */, @@ -2160,6 +2165,7 @@ F3BA808E1CFB2E7A003DC1BA /* FetchedRecordsController.swift in Sources */, 5659F48A1EA8D94E004A4992 /* Utils.swift in Sources */, F3BA807F1CFB2E61003DC1BA /* NSString.swift in Sources */, + 5690C3421D23E82A00E59934 /* Data.swift in Sources */, 56FC987A1D969DEF00E3C842 /* SQLExpression+QueryInterface.swift in Sources */, F3BA806D1CFB2E55003DC1BA /* DatabaseSchemaCache.swift in Sources */, F3BA80841CFB2E67003DC1BA /* StandardLibrary.swift in Sources */, From 820673b78bbab0adf56b387d1641378eb68e7df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 13:23:58 +0200 Subject: [PATCH 19/37] PersistableRecord+Encodable: make it as robust as FetchableRecord+Decodable --- GRDB/Record/PersistableRecord+Encodable.swift | 309 ++++++++++-------- 1 file changed, 167 insertions(+), 142 deletions(-) diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index aa647ea30e..1003e2c69f 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -1,9 +1,7 @@ -import Foundation - private struct PersistableRecordKeyedEncodingContainer : KeyedEncodingContainerProtocol { - let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void + let encode: DatabaseValuePersistenceEncoder - init(encode: @escaping (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { + init(encode: @escaping DatabaseValuePersistenceEncoder) { self.encode = encode } @@ -37,33 +35,28 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn /// - parameter key: The key to associate the value with. /// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format. mutating func encode(_ value: T, forKey key: Key) throws where T : Encodable { - if T.self is DatabaseValueConvertible.Type { + if let dbValueConvertible = value as? DatabaseValueConvertible { // Prefer DatabaseValueConvertible encoding over Decodable. // This allows us to encode Date as String, for example. - encode((value as! DatabaseValueConvertible), key.stringValue) + encode(dbValueConvertible.databaseValue, key.stringValue) } else { do { - try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) - } catch { - // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON - switch error as! EncodingError { - case .invalidValue(_, let context): - if context.debugDescription == "unkeyed encoding is not supported" { - // Support for keyed containers ( [Codable] ) - let encoder = JSONEncoder() - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - encoder.outputFormatting = .sortedKeys - } - let json = try encoder.encode(value) - //the Data from the encoder is guaranteed to convert to String - let modelAsString = String(data: json, encoding: .utf8)! - return encode(modelAsString, key.stringValue) - } else { - throw(error) - } - default: - throw(error) + try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) + } catch is JSONEncodingRequiredError { + // Encode to JSON + let encoder = JSONEncoder() + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys } + let jsonData = try encoder.encode(value) + + // Store JSON String in the database for easier debugging and + // database inspection. Thanks to SQLite weak typing, we won't + // have any trouble decoding this string into data when we + // eventually perform JSON decoding. + // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string + let jsonString = String(data: jsonData, encoding: .utf8)! // json data is guaranteed to convert to String + encode(jsonString, key.stringValue) } } } @@ -122,10 +115,14 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn } private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { - let key: CodingKey - let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void - var codingPath: [CodingKey] { return [key] } + var key: CodingKey + var encode: DatabaseValuePersistenceEncoder + + init(key: CodingKey, encode: @escaping DatabaseValuePersistenceEncoder) { + self.key = key + self.encode = encode + } /// Encodes a null value. /// @@ -165,178 +162,206 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { encode(dbValueConvertible.databaseValue, key.stringValue) } else { do { - try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) - } catch { - // If value.encode does not work e.g. "unkeyed encoding is not supported" then see if model can be stored as JSON - switch error as! EncodingError { - case .invalidValue(_, let context): - if context.debugDescription == "unkeyed encoding is not supported" { - // Support for keyed containers ( [Codable] ) - let encoder = JSONEncoder() - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - encoder.outputFormatting = .sortedKeys - } - let json = try encoder.encode(value) - //the Data from the encoder is guaranteed to convert to String - let modelAsString = String(data: json, encoding: .utf8)! - return encode(modelAsString, key.stringValue) - } else { - throw(error) - } - default: - throw(error) + try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) + } catch is JSONEncodingRequiredError { + // Encode to JSON + let encoder = JSONEncoder() + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + encoder.outputFormatting = .sortedKeys } + let jsonData = try encoder.encode(value) + + // Store JSON String in the database for easier debugging and + // database inspection. Thanks to SQLite weak typing, we won't + // have any trouble decoding this string into data when we + // eventually perform JSON decoding. + // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string + let jsonString = String(data: jsonData, encoding: .utf8)! // json data is guaranteed to convert to String + encode(jsonString, key.stringValue) } } } } -private struct PersistableRecordEncoder : Encoder { - /// The path of coding keys taken to get to this point in encoding. - /// A `nil` value indicates an unkeyed container. - var codingPath: [CodingKey] +private struct DatabaseValueEncoder: Encoder { + var codingPath: [CodingKey] { return [key] } + var userInfo: [CodingUserInfoKey: Any] = [:] + var key: CodingKey + var encode: DatabaseValuePersistenceEncoder - /// Any contextual information set by the user for encoding. - var userInfo: [CodingUserInfoKey : Any] = [:] + init(key: CodingKey, encode: @escaping DatabaseValuePersistenceEncoder) { + self.key = key + self.encode = encode + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + // Keyed values require JSON encoding. + // But we can't throw JSONEncodingRequiredError right here, unfortunately. + // So let's delegate JSONEncodingRequiredError throwing to a + // dedicated container. + return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) + } - let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void + func unkeyedContainer() -> UnkeyedEncodingContainer { + // Unkeyed values require JSON encoding. + // But we can't throw JSONEncodingRequiredError right here, unfortunately. + // So let's delegate JSONEncodingRequiredError throwing to a + // dedicated container. + return JSONEncodingRequiredUnkeyedContainer() + } - init(codingPath: [CodingKey], encode: @escaping (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { - self.codingPath = codingPath + func singleValueContainer() -> SingleValueEncodingContainer { + return DatabaseValueEncodingContainer(key: key, encode: encode) + } +} + +private struct PersistableRecordEncoder: Encoder { + var codingPath: [CodingKey] = [] + var userInfo: [CodingUserInfoKey: Any] = [:] + var encode: DatabaseValuePersistenceEncoder + + init(encode: @escaping DatabaseValuePersistenceEncoder) { self.encode = encode } - /// Returns an encoding container appropriate for holding multiple values keyed by the given key type. - /// - /// - parameter type: The key type to use for the container. - /// - returns: A new keyed encoding container. - /// - precondition: May not be called after a prior `self.unkeyedContainer()` call. - /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { - // Asked for a keyed type: top level required - guard codingPath.isEmpty else { - let error = EncodingError.invalidValue(encode, EncodingError.Context(codingPath: codingPath, debugDescription: "unkeyed encoding is not supported")) - return KeyedEncodingContainer(ThrowingKeyedContainer(error: error)) - } return KeyedEncodingContainer(PersistableRecordKeyedEncodingContainer(encode: encode)) } - /// Returns an encoding container appropriate for holding multiple unkeyed values. - /// - /// - returns: A new empty unkeyed container. - /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. - /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. func unkeyedContainer() -> UnkeyedEncodingContainer { - let error = EncodingError.invalidValue(encode, EncodingError.Context(codingPath: [], debugDescription: "unkeyed encoding is not supported")) - return ThrowingUnkeyedContainer(error: error) + fatalError("unkeyed encoding is not supported") } - /// Returns an encoding container appropriate for holding a single primitive value. - /// - /// - returns: A new empty single value container. - /// - precondition: May not be called after a prior `self.container(keyedBy:)` call. - /// - precondition: May not be called after a prior `self.unkeyedContainer()` call. - /// - precondition: May not be called after a value has been encoded through a previous `self.singleValueContainer()` call. func singleValueContainer() -> SingleValueEncodingContainer { - return DatabaseValueEncodingContainer(key: codingPath.last!, encode: encode) + fatalError("unkeyed encoding is not supported") } } -class ThrowingKeyedContainer: KeyedEncodingContainerProtocol { - let errorMessage: Error - var codingPath: [CodingKey] = [] +private struct JSONEncodingRequiredEncoder: Encoder { + var codingPath: [CodingKey] { return [] } + var userInfo: [CodingUserInfoKey: Any] = [:] + + init() { } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + return JSONEncodingRequiredUnkeyedContainer() + } - init(error: Error) { - errorMessage = error + func singleValueContainer() -> SingleValueEncodingContainer { + return JSONEncodingRequiredSingleValueContainer() } +} + +private struct JSONEncodingRequiredKeyedContainer: KeyedEncodingContainerProtocol { + var codingPath: [CodingKey] { return [] } - func encodeNil(forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Bool, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Int, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Int8, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Int16, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Int32, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Int64, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: UInt, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: UInt8, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: UInt16, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: UInt32, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: UInt64, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Float, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: Double, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: String, forKey key: KeyType) throws { throw errorMessage } - func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw errorMessage } + func encodeNil(forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Bool, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int8, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int16, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int32, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int64, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt8, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt16, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt32, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt64, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Float, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Double, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: String, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } + func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw JSONEncodingRequiredError() } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer where NestedKey : CodingKey { - return KeyedEncodingContainer(ThrowingKeyedContainer(error: errorMessage)) + return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) } + func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { - fatalError("Not implemented") + return JSONEncodingRequiredUnkeyedContainer() } + func superEncoder() -> Encoder { - fatalError("Not implemented") + return JSONEncodingRequiredEncoder() } func superEncoder(forKey key: KeyType) -> Encoder { - fatalError("Not implemented") + return JSONEncodingRequiredEncoder() } } -class ThrowingUnkeyedContainer: UnkeyedEncodingContainer { - let errorMessage: Error - var codingPath: [CodingKey] = [] - var count: Int = 0 - - init(error: Error) { - errorMessage = error - } +private struct JSONEncodingRequiredUnkeyedContainer: UnkeyedEncodingContainer { + var codingPath: [CodingKey] { return [] } + var count: Int { return 0 } - func encode(_ value: Int) throws { throw errorMessage } - func encode(_ value: Int8) throws { throw errorMessage } - func encode(_ value: Int16) throws { throw errorMessage } - func encode(_ value: Int32) throws { throw errorMessage } - func encode(_ value: Int64) throws { throw errorMessage } - func encode(_ value: UInt) throws { throw errorMessage } - func encode(_ value: UInt8) throws { throw errorMessage } - func encode(_ value: UInt16) throws { throw errorMessage } - func encode(_ value: UInt32) throws { throw errorMessage } - func encode(_ value: UInt64) throws { throw errorMessage } - func encode(_ value: Float) throws { throw errorMessage } - func encode(_ value: Double) throws { throw errorMessage } - func encode(_ value: String) throws { throw errorMessage } - func encode(_ value: T) throws where T : Encodable { throw errorMessage } - func encode(_ value: Bool) throws { throw errorMessage } - func encodeNil() throws { throw errorMessage } + func encodeNil() throws { throw JSONEncodingRequiredError() } + func encode(_ value: Bool) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int8) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int16) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int32) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Int64) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt8) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt16) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt32) throws { throw JSONEncodingRequiredError() } + func encode(_ value: UInt64) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Float) throws { throw JSONEncodingRequiredError() } + func encode(_ value: Double) throws { throw JSONEncodingRequiredError() } + func encode(_ value: String) throws { throw JSONEncodingRequiredError() } + func encode(_ value: T) throws where T : Encodable { throw JSONEncodingRequiredError() } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - return KeyedEncodingContainer(ThrowingKeyedContainer(error: errorMessage)) - + return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) } func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - fatalError("Not implemented") - + return self } func superEncoder() -> Encoder { - fatalError("Not implemented") - + return JSONEncodingRequiredEncoder() } } -enum JsonStringError: Error { - case covertStringError(String) +private struct JSONEncodingRequiredSingleValueContainer: SingleValueEncodingContainer { + var codingPath: [CodingKey] { return [] } + + mutating func encodeNil() throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Bool) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: String) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Double) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Float) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Int) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Int8) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Int16) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Int32) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: Int64) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: UInt) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: UInt8) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: UInt16) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: UInt32) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: UInt64) throws { throw JSONEncodingRequiredError() } + mutating func encode(_ value: T) throws where T : Encodable { throw JSONEncodingRequiredError() } } +/// The error that triggers JSON encoding +private struct JSONEncodingRequiredError: Error { } + +private typealias DatabaseValuePersistenceEncoder = (_ value: DatabaseValueConvertible?, _ key: String) -> Void + extension MutablePersistableRecord where Self: Encodable { public func encode(to container: inout PersistenceContainer) { // The inout container parameter won't enter an escaping closure since // SE-0035: https://github.com/apple/swift-evolution/blob/master/proposals/0035-limit-inout-capture.md // // So let's use it in a non-escaping closure: - func encode(_ encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { + func encode(_ encode: DatabaseValuePersistenceEncoder) { withoutActuallyEscaping(encode) { escapableEncode in - let encoder = PersistableRecordEncoder(codingPath: [], encode: escapableEncode) + let encoder = PersistableRecordEncoder(encode: escapableEncode) try! self.encode(to: encoder) } } From 358225b4624b1d658d3e9924ad10e7936651d861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 13:41:53 +0200 Subject: [PATCH 20/37] Cleanup, documentation, and preserve coding paths --- GRDB/Record/FetchableRecord+Decodable.swift | 14 +- GRDB/Record/PersistableRecord+Encodable.swift | 170 +++++++++--------- 2 files changed, 97 insertions(+), 87 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index f66a852dfd..29b19e29ec 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -88,7 +88,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. return try T(from: RowSingleValueDecoder(row: row, columnIndex: index, codingPath: codingPath + [key])) - } catch is JSONDecodingRequiredError { + } catch is JSONRequiredError { guard let data = row.dataNoCopy(atIndex: index) else { fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) } @@ -138,7 +138,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer // value here (string, int, double, data, null). If such an // error happens, we'll switch to JSON decoding. return try T(from: RowSingleValueDecoder(row: row, columnIndex: index, codingPath: codingPath + [key])) - } catch is JSONDecodingRequiredError { + } catch is JSONRequiredError { guard let data = row.dataNoCopy(atIndex: index) else { fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) } @@ -273,11 +273,11 @@ private struct RowDecoder: Decoder { } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw JSONDecodingRequiredError() + throw JSONRequiredError() } func singleValueContainer() throws -> SingleValueDecodingContainer { - throw JSONDecodingRequiredError() + throw JSONRequiredError() } } @@ -288,11 +288,11 @@ private struct RowSingleValueDecoder: Decoder { var userInfo: [CodingUserInfoKey : Any] { return [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - throw JSONDecodingRequiredError() + throw JSONRequiredError() } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw JSONDecodingRequiredError() + throw JSONRequiredError() } func singleValueContainer() throws -> SingleValueDecodingContainer { @@ -301,7 +301,7 @@ private struct RowSingleValueDecoder: Decoder { } /// The error that triggers JSON decoding -private struct JSONDecodingRequiredError: Error { } +private struct JSONRequiredError: Error { } extension FetchableRecord where Self: Decodable { /// Initializes a record from `row`. diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index 1003e2c69f..bb38d1d0a8 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -41,11 +41,16 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn encode(dbValueConvertible.databaseValue, key.stringValue) } else { do { + // This encoding will fail for types that encode into keyed + // or unkeyed containers, because we're encoding a single + // value here (string, int, double, data, null). If such an + // error happens, we'll switch to JSON encoding. try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) - } catch is JSONEncodingRequiredError { + } catch is JSONRequiredError { // Encode to JSON let encoder = JSONEncoder() if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + // guarantee some stability in order to ease record comparison encoder.outputFormatting = .sortedKeys } let jsonData = try encoder.encode(value) @@ -55,7 +60,7 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn // have any trouble decoding this string into data when we // eventually perform JSON decoding. // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string - let jsonString = String(data: jsonData, encoding: .utf8)! // json data is guaranteed to convert to String + let jsonString = String(data: jsonData, encoding: .utf8)! // force unwrap because json data is guaranteed to convert to String encode(jsonString, key.stringValue) } } @@ -162,11 +167,16 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { encode(dbValueConvertible.databaseValue, key.stringValue) } else { do { + // This encoding will fail for types that encode into keyed + // or unkeyed containers, because we're encoding a single + // value here (string, int, double, data, null). If such an + // error happens, we'll switch to JSON encoding. try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) - } catch is JSONEncodingRequiredError { + } catch is JSONRequiredError { // Encode to JSON let encoder = JSONEncoder() if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + // guarantee some stability in order to ease record comparison encoder.outputFormatting = .sortedKeys } let jsonData = try encoder.encode(value) @@ -176,7 +186,7 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { // have any trouble decoding this string into data when we // eventually perform JSON decoding. // TODO: possible optimization: avoid this conversion to string, and store raw data bytes as an SQLite string - let jsonString = String(data: jsonData, encoding: .utf8)! // json data is guaranteed to convert to String + let jsonString = String(data: jsonData, encoding: .utf8)! // force unwrap because json data is guaranteed to convert to String encode(jsonString, key.stringValue) } } @@ -195,19 +205,17 @@ private struct DatabaseValueEncoder: Encoder { } func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - // Keyed values require JSON encoding. - // But we can't throw JSONEncodingRequiredError right here, unfortunately. - // So let's delegate JSONEncodingRequiredError throwing to a - // dedicated container. - return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) + // Keyed values require JSON encoding: we need to throw + // JSONRequiredError. Since we can't throw right from here, let's + // delegate the job to a dedicated container. + return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) } func unkeyedContainer() -> UnkeyedEncodingContainer { - // Unkeyed values require JSON encoding. - // But we can't throw JSONEncodingRequiredError right here, unfortunately. - // So let's delegate JSONEncodingRequiredError throwing to a - // dedicated container. - return JSONEncodingRequiredUnkeyedContainer() + // Keyed values require JSON encoding: we need to throw + // JSONRequiredError. Since we can't throw right from here, let's + // delegate the job to a dedicated container. + return JSONRequiredUnkeyedContainer(codingPath: codingPath) } func singleValueContainer() -> SingleValueEncodingContainer { @@ -237,85 +245,87 @@ private struct PersistableRecordEncoder: Encoder { } } -private struct JSONEncodingRequiredEncoder: Encoder { - var codingPath: [CodingKey] { return [] } +private struct JSONRequiredEncoder: Encoder { + var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey: Any] = [:] - init() { } + init(codingPath: [CodingKey]) { + self.codingPath = codingPath + } func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { - return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) + return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) } func unkeyedContainer() -> UnkeyedEncodingContainer { - return JSONEncodingRequiredUnkeyedContainer() + return JSONRequiredUnkeyedContainer(codingPath: codingPath) } func singleValueContainer() -> SingleValueEncodingContainer { - return JSONEncodingRequiredSingleValueContainer() + return JSONRequiredSingleValueContainer() } } -private struct JSONEncodingRequiredKeyedContainer: KeyedEncodingContainerProtocol { - var codingPath: [CodingKey] { return [] } +private struct JSONRequiredKeyedContainer: KeyedEncodingContainerProtocol { + var codingPath: [CodingKey] - func encodeNil(forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Bool, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int8, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int16, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int32, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int64, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt8, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt16, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt32, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt64, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Float, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Double, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: String, forKey key: KeyType) throws { throw JSONEncodingRequiredError() } - func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw JSONEncodingRequiredError() } + func encodeNil(forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Bool, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int8, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int16, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int32, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Int64, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt8, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt16, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt32, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: UInt64, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Float, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: Double, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: String, forKey key: KeyType) throws { throw JSONRequiredError() } + func encode(_ value: T, forKey key: KeyType) throws where T : Encodable { throw JSONRequiredError() } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: KeyType) -> KeyedEncodingContainer where NestedKey : CodingKey { - return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) + return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath + [key])) } func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { - return JSONEncodingRequiredUnkeyedContainer() + return JSONRequiredUnkeyedContainer(codingPath: codingPath) } func superEncoder() -> Encoder { - return JSONEncodingRequiredEncoder() + return JSONRequiredEncoder(codingPath: codingPath) } func superEncoder(forKey key: KeyType) -> Encoder { - return JSONEncodingRequiredEncoder() + return JSONRequiredEncoder(codingPath: codingPath) } } -private struct JSONEncodingRequiredUnkeyedContainer: UnkeyedEncodingContainer { - var codingPath: [CodingKey] { return [] } +private struct JSONRequiredUnkeyedContainer: UnkeyedEncodingContainer { + var codingPath: [CodingKey] var count: Int { return 0 } - func encodeNil() throws { throw JSONEncodingRequiredError() } - func encode(_ value: Bool) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int8) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int16) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int32) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Int64) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt8) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt16) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt32) throws { throw JSONEncodingRequiredError() } - func encode(_ value: UInt64) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Float) throws { throw JSONEncodingRequiredError() } - func encode(_ value: Double) throws { throw JSONEncodingRequiredError() } - func encode(_ value: String) throws { throw JSONEncodingRequiredError() } - func encode(_ value: T) throws where T : Encodable { throw JSONEncodingRequiredError() } + func encodeNil() throws { throw JSONRequiredError() } + func encode(_ value: Bool) throws { throw JSONRequiredError() } + func encode(_ value: Int) throws { throw JSONRequiredError() } + func encode(_ value: Int8) throws { throw JSONRequiredError() } + func encode(_ value: Int16) throws { throw JSONRequiredError() } + func encode(_ value: Int32) throws { throw JSONRequiredError() } + func encode(_ value: Int64) throws { throw JSONRequiredError() } + func encode(_ value: UInt) throws { throw JSONRequiredError() } + func encode(_ value: UInt8) throws { throw JSONRequiredError() } + func encode(_ value: UInt16) throws { throw JSONRequiredError() } + func encode(_ value: UInt32) throws { throw JSONRequiredError() } + func encode(_ value: UInt64) throws { throw JSONRequiredError() } + func encode(_ value: Float) throws { throw JSONRequiredError() } + func encode(_ value: Double) throws { throw JSONRequiredError() } + func encode(_ value: String) throws { throw JSONRequiredError() } + func encode(_ value: T) throws where T : Encodable { throw JSONRequiredError() } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { - return KeyedEncodingContainer(JSONEncodingRequiredKeyedContainer()) + return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) } func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { @@ -323,33 +333,33 @@ private struct JSONEncodingRequiredUnkeyedContainer: UnkeyedEncodingContainer { } func superEncoder() -> Encoder { - return JSONEncodingRequiredEncoder() + return JSONRequiredEncoder(codingPath: codingPath) } } -private struct JSONEncodingRequiredSingleValueContainer: SingleValueEncodingContainer { +private struct JSONRequiredSingleValueContainer: SingleValueEncodingContainer { var codingPath: [CodingKey] { return [] } - mutating func encodeNil() throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Bool) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: String) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Double) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Float) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Int) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Int8) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Int16) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Int32) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: Int64) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: UInt) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: UInt8) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: UInt16) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: UInt32) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: UInt64) throws { throw JSONEncodingRequiredError() } - mutating func encode(_ value: T) throws where T : Encodable { throw JSONEncodingRequiredError() } + mutating func encodeNil() throws { throw JSONRequiredError() } + mutating func encode(_ value: Bool) throws { throw JSONRequiredError() } + mutating func encode(_ value: String) throws { throw JSONRequiredError() } + mutating func encode(_ value: Double) throws { throw JSONRequiredError() } + mutating func encode(_ value: Float) throws { throw JSONRequiredError() } + mutating func encode(_ value: Int) throws { throw JSONRequiredError() } + mutating func encode(_ value: Int8) throws { throw JSONRequiredError() } + mutating func encode(_ value: Int16) throws { throw JSONRequiredError() } + mutating func encode(_ value: Int32) throws { throw JSONRequiredError() } + mutating func encode(_ value: Int64) throws { throw JSONRequiredError() } + mutating func encode(_ value: UInt) throws { throw JSONRequiredError() } + mutating func encode(_ value: UInt8) throws { throw JSONRequiredError() } + mutating func encode(_ value: UInt16) throws { throw JSONRequiredError() } + mutating func encode(_ value: UInt32) throws { throw JSONRequiredError() } + mutating func encode(_ value: UInt64) throws { throw JSONRequiredError() } + mutating func encode(_ value: T) throws where T : Encodable { throw JSONRequiredError() } } /// The error that triggers JSON encoding -private struct JSONEncodingRequiredError: Error { } +private struct JSONRequiredError: Error { } private typealias DatabaseValuePersistenceEncoder = (_ value: DatabaseValueConvertible?, _ key: String) -> Void From b2234b46a23eecf0b88a6f5e61b1fe739822440e Mon Sep 17 00:00:00 2001 From: Vlad Alexa Date: Wed, 15 Aug 2018 14:04:14 +0100 Subject: [PATCH 21/37] 5f41c57c addition --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7e555a5cff..97921344c5 100644 --- a/README.md +++ b/README.md @@ -2572,14 +2572,14 @@ struct Link : PersistableRecord { GRDB provides default implementations for [`FetchableRecord.init(row:)`](#fetchablerecord-protocol) and [`PersistableRecord.encode(to:)`](#persistablerecord-protocol) for record types that also adopt an archival protocol (`Codable`, `Encodable` or `Decodable`). When all their properties are themselves codable, Swift generates the archiving methods, and you don't need to write them down: ```swift -// Declare a Codable struct or class, nested Codable objects as well as Sets, Arrays, and Optionals are supported +// Declare a Codable struct or class, nested Codable objects as well as Sets, Arrays, Dictionaries and Optionals are all supported struct Player: Codable { let name: String let score: Int let scores: [Int] let lastMedal: PlayerMedal let medals: [PlayerMedal] - //let timeline: [String: PlayerMedal] + let timeline: [String: PlayerMedal] } // A simple Codable that will be nested in a parent Codable From 2d6140a8059f5c90ed72aa78daf9ffc407559951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 15:16:49 +0200 Subject: [PATCH 22/37] Refactor value conversion tests --- GRDB.xcodeproj/project.pbxproj | 16 +- GRDBCipher.xcodeproj/project.pbxproj | 20 +- GRDBCustom.xcodeproj/project.pbxproj | 16 +- .../DatabaseValueConversionTests.swift | 860 ++++++++++-------- .../StatementColumnConvertibleTests.swift | 793 ---------------- 5 files changed, 489 insertions(+), 1216 deletions(-) delete mode 100644 Tests/GRDBTests/StatementColumnConvertibleTests.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 22e81d0484..b518661a99 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -598,7 +598,6 @@ 56D496781D81309E008276D7 /* RecordSubClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238331B9C74A90082EB20 /* RecordSubClassTests.swift */; }; 56D496791D81309E008276D7 /* RecordWithColumnNameManglingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A5E4081BA2BCF900707640 /* RecordWithColumnNameManglingTests.swift */; }; 56D4967C1D8130DB008276D7 /* CGFloatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B7F4291BE14A1900E39BBF /* CGFloatTests.swift */; }; - 56D4967F1D813131008276D7 /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; 56D496801D813131008276D7 /* StatementColumnConvertibleFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */; }; 56D496811D813131008276D7 /* TransactionObserverSavepointsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */; }; 56D496821D813131008276D7 /* TransactionObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5607EFD21BB8254800605DE3 /* TransactionObserverTests.swift */; }; @@ -674,7 +673,6 @@ 56EA86951C91DFE7002BB4DF /* DatabaseReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA86931C91DFE7002BB4DF /* DatabaseReaderTests.swift */; }; 56EA869F1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EA869D1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift */; }; 56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EB0AB11BCD787300A3DC55 /* DataMemoryTests.swift */; }; - 56EE573E1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; 56F0B9921B6001C600A2F135 /* FoundationNSDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F0B98E1B6001C600A2F135 /* FoundationNSDateTests.swift */; }; 56F26C1C1CEE3F32007969C4 /* RowAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F03C11CE5D3AA00DE108F /* RowAdapterTests.swift */; }; 56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */; }; @@ -1122,7 +1120,6 @@ 56EA869D1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolReadOnlyTests.swift; sourceTree = ""; }; 56EB0AB11BCD787300A3DC55 /* DataMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataMemoryTests.swift; sourceTree = ""; }; 56ED8A7E1DAB8D6800BD0ABC /* FTS5WrapperTokenizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5WrapperTokenizerTests.swift; sourceTree = ""; }; - 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleTests.swift; sourceTree = ""; }; 56F0B98E1B6001C600A2F135 /* FoundationNSDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDateTests.swift; sourceTree = ""; }; 56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = ""; }; 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0.101.1.swift"; sourceTree = ""; }; @@ -1587,7 +1584,7 @@ 569C1EB11CF07DDD0042627B /* SchedulingWatchdogTests.swift */, 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */, 56A238201B9C74A90082EB20 /* Statement */, - 56EE573B1BB317B7007A6A95 /* StatementColumnConvertible */, + 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, 5607EFD11BB8253300605DE3 /* TransactionObserver */, ); name = Core; @@ -1806,15 +1803,6 @@ name = Cursor; sourceTree = ""; }; - 56EE573B1BB317B7007A6A95 /* StatementColumnConvertible */ = { - isa = PBXGroup; - children = ( - 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, - 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */, - ); - name = StatementColumnConvertible; - sourceTree = ""; - }; 56F0B98C1B6001C600A2F135 /* Foundation */ = { isa = PBXGroup; children = ( @@ -2591,7 +2579,6 @@ 5657AB4A1D108BA9006283EF /* FoundationNSNullTests.swift in Sources */, 569531351C919DF200CF1A2B /* DatabasePoolCollationTests.swift in Sources */, 56A2385A1B9C74A90082EB20 /* RecordPrimaryKeySingleTests.swift in Sources */, - 56EE573E1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift in Sources */, 562756471E963AAC0035B653 /* DatabaseWriterTests.swift in Sources */, 569531381C919DF700CF1A2B /* DatabasePoolFunctionTests.swift in Sources */, 56A238481B9C74A90082EB20 /* RowCopiedFromStatementTests.swift in Sources */, @@ -2876,7 +2863,6 @@ 56D4965C1D81304E008276D7 /* FoundationNSNullTests.swift in Sources */, 5653EAD820944B4F00F46237 /* AssociationBelongsToRowScopeTests.swift in Sources */, 562205F11E420E47005860AC /* DatabasePoolReleaseMemoryTests.swift in Sources */, - 56D4967F1D813131008276D7 /* StatementColumnConvertibleTests.swift in Sources */, 5653EAF420944B4F00F46237 /* AssociationParallelSQLTests.swift in Sources */, 56D496971D81317B008276D7 /* DatabaseReaderTests.swift in Sources */, 56D496911D81316E008276D7 /* RowFromStatementTests.swift in Sources */, diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index 04f6e7ab9d..29826951bd 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -51,7 +51,6 @@ 560FC56D1CB00B880014AA8E /* DatabaseMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238241B9C74A90082EB20 /* DatabaseMigratorTests.swift */; }; 560FC5701CB00B880014AA8E /* DatabasePoolCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531331C919DF200CF1A2B /* DatabasePoolCollationTests.swift */; }; 560FC5711CB00B880014AA8E /* RecordPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2382B1B9C74A90082EB20 /* RecordPrimaryKeySingleTests.swift */; }; - 560FC5721CB00B880014AA8E /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; 560FC5741CB00B880014AA8E /* DatabasePoolFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531361C919DF700CF1A2B /* DatabasePoolFunctionTests.swift */; }; 560FC5761CB00B880014AA8E /* RowCopiedFromStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2381F1B9C74A90082EB20 /* RowCopiedFromStatementTests.swift */; }; 560FC5771CB00B880014AA8E /* DatabaseQueueSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */; }; @@ -386,7 +385,6 @@ 5671562B1CB16729007DC145 /* DatabaseMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238241B9C74A90082EB20 /* DatabaseMigratorTests.swift */; }; 5671562E1CB16729007DC145 /* DatabasePoolCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531331C919DF200CF1A2B /* DatabasePoolCollationTests.swift */; }; 5671562F1CB16729007DC145 /* RecordPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2382B1B9C74A90082EB20 /* RecordPrimaryKeySingleTests.swift */; }; - 567156301CB16729007DC145 /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; 567156321CB16729007DC145 /* DatabasePoolFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531361C919DF700CF1A2B /* DatabasePoolFunctionTests.swift */; }; 567156341CB16729007DC145 /* RowCopiedFromStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2381F1B9C74A90082EB20 /* RowCopiedFromStatementTests.swift */; }; 567156351CB16729007DC145 /* DatabaseQueueSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */; }; @@ -641,7 +639,6 @@ 56AFCA3B1CB1AA9900F48B96 /* DatabaseMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238241B9C74A90082EB20 /* DatabaseMigratorTests.swift */; }; 56AFCA3E1CB1AA9900F48B96 /* DatabasePoolCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531331C919DF200CF1A2B /* DatabasePoolCollationTests.swift */; }; 56AFCA3F1CB1AA9900F48B96 /* RecordPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2382B1B9C74A90082EB20 /* RecordPrimaryKeySingleTests.swift */; }; - 56AFCA411CB1AA9900F48B96 /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; 56AFCA421CB1AA9900F48B96 /* DatabasePoolFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531361C919DF700CF1A2B /* DatabasePoolFunctionTests.swift */; }; 56AFCA441CB1AA9900F48B96 /* RowCopiedFromStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2381F1B9C74A90082EB20 /* RowCopiedFromStatementTests.swift */; }; 56AFCA451CB1AA9900F48B96 /* DatabaseQueueSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */; }; @@ -701,7 +698,6 @@ 56AFCA941CB1ABC800F48B96 /* DatabaseMigratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238241B9C74A90082EB20 /* DatabaseMigratorTests.swift */; }; 56AFCA971CB1ABC800F48B96 /* DatabasePoolCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531331C919DF200CF1A2B /* DatabasePoolCollationTests.swift */; }; 56AFCA981CB1ABC800F48B96 /* RecordPrimaryKeySingleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2382B1B9C74A90082EB20 /* RecordPrimaryKeySingleTests.swift */; }; - 56AFCA9A1CB1ABC800F48B96 /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; 56AFCA9B1CB1ABC800F48B96 /* DatabasePoolFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531361C919DF700CF1A2B /* DatabasePoolFunctionTests.swift */; }; 56AFCA9D1CB1ABC800F48B96 /* RowCopiedFromStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2381F1B9C74A90082EB20 /* RowCopiedFromStatementTests.swift */; }; 56AFCA9E1CB1ABC800F48B96 /* DatabaseQueueSchemaCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 569531231C90878D00CF1A2B /* DatabaseQueueSchemaCacheTests.swift */; }; @@ -1184,7 +1180,6 @@ 56EA869D1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolReadOnlyTests.swift; sourceTree = ""; }; 56EB0AB11BCD787300A3DC55 /* DataMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataMemoryTests.swift; sourceTree = ""; }; 56ED8A7E1DAB8D6800BD0ABC /* FTS5WrapperTokenizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5WrapperTokenizerTests.swift; sourceTree = ""; }; - 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleTests.swift; sourceTree = ""; }; 56F0B98E1B6001C600A2F135 /* FoundationNSDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDateTests.swift; sourceTree = ""; }; 56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = ""; }; 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0.101.1.swift"; sourceTree = ""; }; @@ -1636,7 +1631,7 @@ 569C1EB11CF07DDD0042627B /* SchedulingWatchdogTests.swift */, 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */, 56A238201B9C74A90082EB20 /* Statement */, - 56EE573B1BB317B7007A6A95 /* StatementColumnConvertible */, + 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, 5607EFD11BB8253300605DE3 /* TransactionObserver */, ); name = Core; @@ -1792,15 +1787,6 @@ name = Cursor; sourceTree = ""; }; - 56EE573B1BB317B7007A6A95 /* StatementColumnConvertible */ = { - isa = PBXGroup; - children = ( - 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, - 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */, - ); - name = StatementColumnConvertible; - sourceTree = ""; - }; 56F0B98C1B6001C600A2F135 /* Foundation */ = { isa = PBXGroup; children = ( @@ -2288,7 +2274,6 @@ 56EA63CC209C7F31009715B8 /* DerivableRequestTests.swift in Sources */, 560FC5701CB00B880014AA8E /* DatabasePoolCollationTests.swift in Sources */, 560FC5711CB00B880014AA8E /* RecordPrimaryKeySingleTests.swift in Sources */, - 560FC5721CB00B880014AA8E /* StatementColumnConvertibleTests.swift in Sources */, 5674A7191F3087710095F066 /* DatabaseValueConvertibleDecodableTests.swift in Sources */, 56B021CA1D8C0D3900B239BB /* MutablePersistableRecordPersistenceConflictPolicyTests.swift in Sources */, 562205F61E420E48005860AC /* DatabaseQueueReleaseMemoryTests.swift in Sources */, @@ -2447,7 +2432,6 @@ 5671562E1CB16729007DC145 /* DatabasePoolCollationTests.swift in Sources */, 5671562F1CB16729007DC145 /* RecordPrimaryKeySingleTests.swift in Sources */, 5653EBE120961FE800F46237 /* AssociationBelongsToSQLTests.swift in Sources */, - 567156301CB16729007DC145 /* StatementColumnConvertibleTests.swift in Sources */, 56EA63CD209C7F31009715B8 /* DerivableRequestTests.swift in Sources */, 56C3F7551CF9F12400F6A361 /* DatabaseSavepointTests.swift in Sources */, 5672DE5B1CDB72520022BA81 /* DatabaseQueueBackupTests.swift in Sources */, @@ -2725,7 +2709,6 @@ 56FF455B1D2CDA5200F21EF9 /* RecordUniqueIndexTests.swift in Sources */, 56EA63CE209C7F31009715B8 /* DerivableRequestTests.swift in Sources */, 56AFCA3F1CB1AA9900F48B96 /* RecordPrimaryKeySingleTests.swift in Sources */, - 56AFCA411CB1AA9900F48B96 /* StatementColumnConvertibleTests.swift in Sources */, 56AFCA421CB1AA9900F48B96 /* DatabasePoolFunctionTests.swift in Sources */, 5674A7181F3087710095F066 /* DatabaseValueConvertibleDecodableTests.swift in Sources */, 56AFCA441CB1AA9900F48B96 /* RowCopiedFromStatementTests.swift in Sources */, @@ -2896,7 +2879,6 @@ 562393361DEDFC5700A6B01F /* AnyCursorTests.swift in Sources */, 5653EBDF20961FE800F46237 /* AssociationParallelRowScopesTests.swift in Sources */, 5698ACDD1DA925430056AF8C /* RowTestCase.swift in Sources */, - 56AFCA9A1CB1ABC800F48B96 /* StatementColumnConvertibleTests.swift in Sources */, 5653EBCF20961FE800F46237 /* AssociationHasOneSQLTests.swift in Sources */, 562756491E963AAC0035B653 /* DatabaseWriterTests.swift in Sources */, 56AFCA9B1CB1ABC800F48B96 /* DatabasePoolFunctionTests.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 998fa7c85b..0ff2908ebd 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -487,9 +487,7 @@ F3BA80F01CFB3017003DC1BA /* FetchableRecordTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565B0FEE1BBC7D980098DE03 /* FetchableRecordTests.swift */; }; F3BA80F11CFB3019003DC1BA /* DatabaseSavepointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */; }; F3BA80F21CFB301A003DC1BA /* DatabaseSavepointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */; }; - F3BA80F31CFB301D003DC1BA /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; F3BA80F41CFB301D003DC1BA /* StatementColumnConvertibleFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */; }; - F3BA80F51CFB301E003DC1BA /* StatementColumnConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */; }; F3BA80F61CFB301E003DC1BA /* StatementColumnConvertibleFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */; }; F3BA80F71CFB3021003DC1BA /* SelectStatementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A238211B9C74A90082EB20 /* SelectStatementTests.swift */; }; F3BA80F81CFB3021003DC1BA /* StatementArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56DE7B101C3D93ED00861EB8 /* StatementArgumentsTests.swift */; }; @@ -843,7 +841,6 @@ 56EA869D1C932597002BB4DF /* DatabasePoolReadOnlyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabasePoolReadOnlyTests.swift; sourceTree = ""; }; 56EB0AB11BCD787300A3DC55 /* DataMemoryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataMemoryTests.swift; sourceTree = ""; }; 56ED8A7E1DAB8D6800BD0ABC /* FTS5WrapperTokenizerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5WrapperTokenizerTests.swift; sourceTree = ""; }; - 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatementColumnConvertibleTests.swift; sourceTree = ""; }; 56F0B98E1B6001C600A2F135 /* FoundationNSDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationNSDateTests.swift; sourceTree = ""; }; 56F3E7481E66F83A00BF0F01 /* ResultCodeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultCodeTests.swift; sourceTree = ""; }; 56F3E7621E67F8C100BF0F01 /* Fixits-0.101.1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fixits-0.101.1.swift"; sourceTree = ""; }; @@ -1278,7 +1275,7 @@ 569C1EB11CF07DDD0042627B /* SchedulingWatchdogTests.swift */, 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */, 56A238201B9C74A90082EB20 /* Statement */, - 56EE573B1BB317B7007A6A95 /* StatementColumnConvertible */, + 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, 5607EFD11BB8253300605DE3 /* TransactionObserver */, ); name = Core; @@ -1434,15 +1431,6 @@ name = Cursor; sourceTree = ""; }; - 56EE573B1BB317B7007A6A95 /* StatementColumnConvertible */ = { - isa = PBXGroup; - children = ( - 56E8CE0F1BB4FE5B00828BEC /* StatementColumnConvertibleFetchTests.swift */, - 56EE573C1BB317B7007A6A95 /* StatementColumnConvertibleTests.swift */, - ); - name = StatementColumnConvertible; - sourceTree = ""; - }; 56F0B98C1B6001C600A2F135 /* Foundation */ = { isa = PBXGroup; children = ( @@ -1985,7 +1973,6 @@ 56CC9235201E009100CB597E /* DropWhileCursorTests.swift in Sources */, 56D507651F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift in Sources */, 5657AB651D108BA9006283EF /* FoundationNSURLTests.swift in Sources */, - F3BA80F31CFB301D003DC1BA /* StatementColumnConvertibleTests.swift in Sources */, 5657AB6D1D108BA9006283EF /* FoundationURLTests.swift in Sources */, 567F45AF1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, F3BA804C1CFB2B24003DC1BA /* GRDBTestCase.swift in Sources */, @@ -2261,7 +2248,6 @@ 56D507611F6BAE8600AE1C5B /* PrimaryKeyInfoTests.swift in Sources */, F3BA80B01CFB2FB2003DC1BA /* DatabaseCollationTests.swift in Sources */, 56C7A6AE1D2DFF6100EFB0C2 /* FoundationNSDateTests.swift in Sources */, - F3BA80F51CFB301E003DC1BA /* StatementColumnConvertibleTests.swift in Sources */, 567F45AB1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, F3BA80A61CFB2F91003DC1BA /* GRDBTestCase.swift in Sources */, 5657AB411D108BA9006283EF /* FoundationNSDataTests.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index 78c178762d..5f3c629f50 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -7,7 +7,9 @@ import XCTest import GRDB #endif -enum SQLiteStorageClass { +// TODO: test conversions from invalid UTF-8 blob to string + +private enum SQLiteStorageClass { case null case integer case real @@ -15,25 +17,71 @@ enum SQLiteStorageClass { case blob } -extension DatabaseValue { +private extension DatabaseValue { var storageClass: SQLiteStorageClass { switch storage { - case .null: - return .null - case .int64: - return .integer - case .double: - return .real - case .string: - return .text - case .blob: - return .blob + case .null: return .null + case .int64: return .integer + case .double: return .real + case .string: return .text + case .blob: return .blob } } } class DatabaseValueConversionTests : GRDBTestCase { + private func assertDecoding( + _ db: Database, + _ sql: String, + _ type: T.Type, + expectedSQLiteConversion: T?, + expectedDatabaseValueConversion: T?, + file: StaticString = #file, + line: UInt = #line) throws + { + func stringRepresentation(_ value: T?) -> String { + guard let value = value else { return "nil" } + return String(reflecting: value) + } + + do { + // test T.fetchOne + let sqliteConversion = try T.fetchOne(db, sql) + XCTAssert( + sqliteConversion == expectedSQLiteConversion, + "unexpected SQLite conversion: \(stringRepresentation(sqliteConversion)) instead of \(stringRepresentation(expectedSQLiteConversion))", + file: file, line: line) + } + + do { + // test row[0] as T? + let sqliteConversion = try Row.fetchCursor(db, sql).map { $0[0] as T? }.next()! + XCTAssert( + sqliteConversion == expectedSQLiteConversion, + "unexpected SQLite conversion: \(stringRepresentation(sqliteConversion)) instead of \(stringRepresentation(expectedSQLiteConversion))", + file: file, line: line) + } + + do { + // test row[0] as T + let sqliteConversion = try Row.fetchCursor(db, sql).map { $0.hasNull(atIndex: 0) ? nil : ($0[0] as T) }.next()! + XCTAssert( + sqliteConversion == expectedSQLiteConversion, + "unexpected SQLite conversion: \(stringRepresentation(sqliteConversion)) instead of \(stringRepresentation(expectedSQLiteConversion))", + file: file, line: line) + } + + do { + // test T.fromDatabaseValue + let dbValueConversion = try T.fromDatabaseValue(DatabaseValue.fetchOne(db, sql)!) + XCTAssert( + dbValueConversion == expectedDatabaseValueConversion, + "unexpected SQLite conversion: \(stringRepresentation(dbValueConversion)) instead of \(stringRepresentation(expectedDatabaseValueConversion))", + file: file, line: line) + } + } + // Datatypes In SQLite Version 3: https://www.sqlite.org/datatype3.html override func setup(_ dbWriter: DatabaseWriter) throws { @@ -61,22 +109,35 @@ class DatabaseValueConversionTests : GRDBTestCase { let dbQueue = try makeDatabaseQueue() + // Null is turned to null + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (textAffinity) VALUES (NULL)") + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .null) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + return .rollback + } + // Int is turned to Text try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0 as Int]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: "0") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: "0".data(using: .utf8)) return .rollback } @@ -84,18 +145,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0 as Int64]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: "0") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: "0".data(using: .utf8)) return .rollback } @@ -103,37 +161,48 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0 as Int32]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: "0") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: "0".data(using: .utf8)) return .rollback } - // Double is turned to Real + // Double is turned to Text try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0.0]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "0.0") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "0.0".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0.0", expectedDatabaseValueConversion: "0.0") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0.0".data(using: .utf8), expectedDatabaseValueConversion: "0.0".data(using: .utf8)) + + return .rollback + } + + // Empty string is turned to Text + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [""]) + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "", expectedDatabaseValueConversion: "") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: Data(), expectedDatabaseValueConversion: Data()) return .rollback } @@ -141,18 +210,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["3.0e+5"]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "3.0e+5") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "3.0e+5".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:3, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 3, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 3, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "3.0e+5", expectedDatabaseValueConversion: "3.0e+5") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "3.0e+5".data(using: .utf8), expectedDatabaseValueConversion: "3.0e+5".data(using: .utf8)) return .rollback } @@ -160,18 +226,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -179,18 +242,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - let dbValue = try Row.fetchOne(db, "SELECT textAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.blob) - - // Check GRDB conversions from Blob storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } } @@ -215,7 +275,7 @@ class DatabaseValueConversionTests : GRDBTestCase { // > an integer. Hence, the string '3.0e+5' is stored in a column with // > NUMERIC affinity as the integer 300000, not as the floating point // > value 300000.0. - + try testNumericAffinity("numericAffinity") } @@ -244,22 +304,35 @@ class DatabaseValueConversionTests : GRDBTestCase { let dbQueue = try makeDatabaseQueue() + // Null is turned to null + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (realAffinity) VALUES (NULL)") + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .null) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + return .rollback + } + // Int is turned to Real try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [0 as Int]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0.0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0.0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -267,18 +340,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [0 as Int64]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0.0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0.0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -286,18 +356,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [0 as Int32]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0.0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0.0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -305,18 +372,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [3.0e5]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 300000) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(300000)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(300000)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, Double(300000)) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "300000.0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "300000.0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -324,15 +388,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [1.0e20]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 1e20) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) +// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) + return .rollback + } + + // Empty string is turned to Text + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [""]) + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "", expectedDatabaseValueConversion: "") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: Data(), expectedDatabaseValueConversion: Data()) return .rollback } @@ -340,18 +420,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["3.0e+5"]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 300000) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(300000)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(300000)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, Double(300000)) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "300000.0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "300000.0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -359,15 +436,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["1.0e+20"]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage: (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 1e20) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) +// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -375,18 +452,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -394,18 +468,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - let dbValue = try Row.fetchOne(db, "SELECT realAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.blob) - - // Check GRDB conversions from Blob storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } } @@ -419,22 +490,35 @@ class DatabaseValueConversionTests : GRDBTestCase { let dbQueue = try makeDatabaseQueue() + // Null is turned to null + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (NULL)") + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .null) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + return .rollback + } + // Int is turned to Integer try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0 as Int]) - let dbValue = try Row.fetchOne(db, "SELECT noneAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -442,18 +526,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0 as Int64]) - let dbValue = try Row.fetchOne(db, "SELECT noneAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -461,18 +542,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0 as Int32]) - let dbValue = try Row.fetchOne(db, "SELECT noneAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -480,18 +558,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0.0]) - let dbValue = try Row.fetchOne(db, "SELECT noneAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0.0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0.0".data(using: .utf8), expectedDatabaseValueConversion: nil) + return .rollback + } + + // Empty string is turned to Text + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [""]) + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "", expectedDatabaseValueConversion: "") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: Data(), expectedDatabaseValueConversion: Data()) return .rollback } @@ -499,18 +590,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["3.0e+5"]) - let dbValue = try Row.fetchOne(db, "SELECT noneAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "3.0e+5") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "3.0e+5".data(using: .utf8)) - + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:3, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 3, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 3, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "3.0e+5", expectedDatabaseValueConversion: "3.0e+5") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "3.0e+5".data(using: .utf8), expectedDatabaseValueConversion: "3.0e+5".data(using: .utf8)) + return .rollback + } + + // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -518,18 +622,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - let dbValue = try Row.fetchOne(db, "SELECT noneAffinity FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.blob) - - // Check GRDB conversions from Blob storage - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } } @@ -557,22 +658,35 @@ class DatabaseValueConversionTests : GRDBTestCase { let dbQueue = try makeDatabaseQueue() + // Null is turned to null + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (\(columnName)) VALUES (NULL)") + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .null) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + return .rollback + } + // Int is turned to Integer try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [0 as Int]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -580,18 +694,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [0 as Int64]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -599,18 +710,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [0 as Int32]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, false) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 0) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(0)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(0)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 0.0) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: false) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: 0) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "0", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "0".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -618,18 +726,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [3.0e5]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 300000) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(300000)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(300000)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, Double(300000)) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "300000", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "300000".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -637,15 +742,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [1.0e20]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 1e20) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) +// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) + return .rollback + } + + // Empty string is turned to Text + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [""]) + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "", expectedDatabaseValueConversion: "") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: Data(), expectedDatabaseValueConversion: Data()) return .rollback } @@ -653,18 +774,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["3.0e+5"]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.integer) - - // Check GRDB conversions from Integer storage - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Int.fromDatabaseValue(dbValue)!, 300000) - XCTAssertEqual(Int32.fromDatabaseValue(dbValue)!, Int32(300000)) - XCTAssertEqual(Int64.fromDatabaseValue(dbValue)!, Int64(300000)) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, Double(300000)) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .integer) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 300000, expectedDatabaseValueConversion: 300000) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "300000", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "300000".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -672,15 +790,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["1.0e+20"]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.real) - - // Check GRDB conversions from Real storage: (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual(Bool.fromDatabaseValue(dbValue)!, true) - XCTAssertEqual(Double.fromDatabaseValue(dbValue)!, 1e20) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) +// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) +// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) return .rollback } @@ -688,18 +806,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.text) - - // Check GRDB conversions from Text storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } @@ -707,18 +822,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try dbQueue.inTransaction { db in try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - let dbValue = try Row.fetchOne(db, "SELECT \(columnName) FROM `values`")!.first!.1 // first is (columnName, dbValue) - XCTAssertEqual(dbValue.storageClass, SQLiteStorageClass.blob) - - // Check GRDB conversions from Blob storage: - XCTAssertTrue(Bool.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int32.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Int64.fromDatabaseValue(dbValue) == nil) - XCTAssertTrue(Double.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(String.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) return .rollback } } diff --git a/Tests/GRDBTests/StatementColumnConvertibleTests.swift b/Tests/GRDBTests/StatementColumnConvertibleTests.swift deleted file mode 100644 index 3dafe1b82e..0000000000 --- a/Tests/GRDBTests/StatementColumnConvertibleTests.swift +++ /dev/null @@ -1,793 +0,0 @@ -import XCTest -#if GRDBCIPHER - import GRDBCipher -#elseif GRDBCUSTOMSQLITE - import GRDBCustomSQLite -#else - import GRDB -#endif - -class StatementColumnConvertibleTests : GRDBTestCase { - - // Datatypes In SQLite Version 3: https://www.sqlite.org/datatype3.html - - override func setup(_ dbWriter: DatabaseWriter) throws { - var migrator = DatabaseMigrator() - migrator.registerMigration("createPersons") { db in - try db.execute(""" - CREATE TABLE `values` ( - integerAffinity INTEGER, - textAffinity TEXT, - noneAffinity BLOB, - realAffinity DOUBLE, - numericAffinity NUMERIC) - """) - } - try migrator.migrate(dbWriter) - } - - func testTextAffinity() throws { - // https://www.sqlite.org/datatype3.html - // - // > A column with TEXT affinity stores all data using storage classes - // > NULL, TEXT or BLOB. If numerical data is inserted into a column - // > with TEXT affinity it is converted into text form before being - // > stored. - - let dbQueue = try makeDatabaseQueue() - - // Null is turned to null - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (NULL)") - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?) - return .rollback - } - - // Int is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0 as Int]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int64 is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0 as Int64]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int32 is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0 as Int32]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Double is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [0.0]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "0.0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "0.0") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, "0.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), "0.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Empty string is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [""]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, Data()) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), Data()) // incompatible with DatabaseValue conversion - return .rollback - } - - // "3.0e+5" is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["3.0e+5"]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), true) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 3) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 3) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 3) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 300000.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "3.0e+5") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "3.0e+5") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, "3.0e+5".data(using: .utf8)) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), "3.0e+5".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?)!, "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Blob is turned to Blob - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - // Check SQLite conversions from Blob storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as String?), "'fooéı👨👨🏿🇫🇷🇨🇮'") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT textAffinity FROM `values`").next()![0] as Data?), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - return .rollback - } - } - - func testNumericAffinity() throws { - // https://www.sqlite.org/datatype3.html - // - // > A column with NUMERIC affinity may contain values using all five - // > storage classes. When text data is inserted into a NUMERIC column, - // > the storage class of the text is converted to INTEGER or REAL (in - // > order of preference) if such conversion is lossless and reversible. - // > For conversions between TEXT and REAL storage classes, SQLite - // > considers the conversion to be lossless and reversible if the first - // > 15 significant decimal digits of the number are preserved. If the - // > lossless conversion of TEXT to INTEGER or REAL is not possible then - // > the value is stored using the TEXT storage class. No attempt is - // > made to convert NULL or BLOB values. - // > - // > A string might look like a floating-point literal with a decimal - // > point and/or exponent notation but as long as the value can be - // > expressed as an integer, the NUMERIC affinity will convert it into - // > an integer. Hence, the string '3.0e+5' is stored in a column with - // > NUMERIC affinity as the integer 300000, not as the floating point - // > value 300000.0. - - try testNumericAffinity("numericAffinity") - } - - func testIntegerAffinity() throws { - // https://www.sqlite.org/datatype3.html - // - // > A column that uses INTEGER affinity behaves the same as a column - // > with NUMERIC affinity. The difference between INTEGER and NUMERIC - // > affinity is only evident in a CAST expression. - - try testNumericAffinity("integerAffinity") - } - - func testRealAffinity() throws { - // https://www.sqlite.org/datatype3.html - // - // > A column with REAL affinity behaves like a column with NUMERIC - // > affinity except that it forces integer values into floating point - // > representation. (As an internal optimization, small floating point - // > values with no fractional component and stored in columns with REAL - // > affinity are written to disk as integers in order to take up less - // > space and are automatically converted back into floating point as - // > the value is read out. This optimization is completely invisible at - // > the SQL level and can only be detected by examining the raw bits of - // > the database file.) - - let dbQueue = try makeDatabaseQueue() - - // Null is turned to null - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (NULL)") - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?) - return .rollback - } - - // Int is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [0 as Int]) - // Check SQLite conversions from Real storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "0.0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "0.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int64 is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [0 as Int64]) - // Check SQLite conversions from Real storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "0.0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "0.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int32 is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [0 as Int32]) - // Check SQLite conversions from Real storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "0.0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "0.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // 3.0e5 Double is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [3.0e5]) - // Check SQLite conversions from Real storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?)!, 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int), 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?)!, Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32), Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?)!, Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64), Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "300000.0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "300000.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // 1.0e20 Double is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [1.0e20]) - // Check SQLite conversions from Real storage (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "1.0e+20") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "1.0e+20".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Empty string is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [""]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?)!, "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String), "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?)!, Data()) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data), Data()) // incompatible with DatabaseValue conversion - return .rollback - } - - // "3.0e+5" is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["3.0e+5"]) - // Check SQLite conversions from Real storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?)!, 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int), 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?)!, Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32), Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?)!, Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64), Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "300000.0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "300000.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // "1.0e+20" is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["1.0e+20"]) - // Check SQLite conversions from Real storage: (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?)!, 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double), 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "1.0e+20") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "1.0e+20".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Blob is turned to Blob - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - // Check SQLite conversions from Blob storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as String?), "'fooéı👨👨🏿🇫🇷🇨🇮'") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT realAffinity FROM `values`").next()![0] as Data?), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - return .rollback - } - } - - func testNoneAffinity() throws { - // https://www.sqlite.org/datatype3.html - // - // > A column with affinity NONE does not prefer one storage class over - // > another and no attempt is made to coerce data from one storage - // > class into another. - - let dbQueue = try makeDatabaseQueue() - - // Null is turned to null - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (NULL)") - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?) - return .rollback - } - - // Int is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0 as Int]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?), "0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int64 is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0 as Int64]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?), "0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int32 is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0 as Int32]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?), "0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Double is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [0.0]) - // Check SQLite conversions from Real storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?), "0.0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?), "0.0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Empty string is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [""]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?)!, "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String), "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?)!, Data()) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data), Data()) // incompatible with DatabaseValue conversion - return .rollback - } - - // "3.0e+5" is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["3.0e+5"]) - // Check SQLite conversions from Text storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?), true) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?), 3) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?), 3) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?), 3) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?), 300000.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?)!, "3.0e+5") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String), "3.0e+5") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?), "3.0e+5".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Blob is turned to Blob - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - // Check SQLite conversions from Blob storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as String?), "'fooéı👨👨🏿🇫🇷🇨🇮'") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT noneAffinity FROM `values`").next()![0] as Data?), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - return .rollback - } - } - - func testNumericAffinity(_ columnName: String) throws { - // https://www.sqlite.org/datatype3.html - // - // > A column with NUMERIC affinity may contain values using all five - // > storage classes. When text data is inserted into a NUMERIC column, - // > the storage class of the text is converted to INTEGER or REAL (in - // > order of preference) if such conversion is lossless and reversible. - // > For conversions between TEXT and REAL storage classes, SQLite - // > considers the conversion to be lossless and reversible if the first - // > 15 significant decimal digits of the number are preserved. If the - // > lossless conversion of TEXT to INTEGER or REAL is not possible then - // > the value is stored using the TEXT storage class. No attempt is - // > made to convert NULL or BLOB values. - // > - // > A string might look like a floating-point literal with a decimal - // > point and/or exponent notation but as long as the value can be - // > expressed as an integer, the NUMERIC affinity will convert it into - // > an integer. Hence, the string '3.0e+5' is stored in a column with - // > NUMERIC affinity as the integer 300000, not as the floating point - // > value 300000.0. - - let dbQueue = try makeDatabaseQueue() - - // Null is turned to null - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (NULL)") - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?) - XCTAssertNil(try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?) - return .rollback - } - - // Int is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [0 as Int]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int64 is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [0 as Int64]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Int32 is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [0 as Int32]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), false) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?)!, 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int), 0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?)!, Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32), Int32(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?)!, Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64), Int64(0)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), 0.0) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "0") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "0".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // 3.0e5 Double is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [3.0e5]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?)!, 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int), 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?)!, Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32), Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?)!, Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64), Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "300000") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "300000".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // 1.0e20 Double is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [1.0e20]) - // Check SQLite conversions from Real storage (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "1.0e+20") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "1.0e+20".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Empty string is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [""]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?)!, "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String), "") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?)!, Data()) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data), Data()) // incompatible with DatabaseValue conversion - return .rollback - } - - // "3.0e+5" is turned to Integer - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["3.0e+5"]) - // Check SQLite conversions from Integer storage - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?)!, 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int), 300000) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?)!, Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32), Int32(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?)!, Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64), Int64(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), Double(300000)) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "300000") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "300000".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // "1.0e+20" is turned to Real - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["1.0e+20"]) - // Check SQLite conversions from Real storage: (avoid Int, Int32 and Int64 since 1.0e20 does not fit) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?)!, true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool), true) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?)!, 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double), 1e20) - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "1.0e+20") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "1.0e+20".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) - // Check SQLite conversions from Text storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?)!, "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String), "'fooéı👨👨🏿🇫🇷🇨🇮'") - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) // incompatible with DatabaseValue conversion - return .rollback - } - - // Blob is turned to Blob - - try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) - // Check SQLite conversions from Blob storage: - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Bool?), false) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int32?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Int64?), 0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Double?), 0.0) // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as String?), "'fooéı👨👨🏿🇫🇷🇨🇮'") // incompatible with DatabaseValue conversion - XCTAssertEqual((try Row.fetchCursor(db, "SELECT \(columnName) FROM `values`").next()![0] as Data?), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - return .rollback - } - } -} From de4f57c2aa7d70f37446a2ef820537b464559c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 16:09:45 +0200 Subject: [PATCH 23/37] Test decoding of non-UTF8 data --- .../DatabaseValueConversionTests.swift | 132 +++++++++++++----- 1 file changed, 100 insertions(+), 32 deletions(-) diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index 5f3c629f50..0ced418404 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -29,6 +29,10 @@ private extension DatabaseValue { } } +private let emojiString = "'fooéı👨👨🏿🇫🇷🇨🇮'" +private let emojiData = emojiString.data(using: .utf8) +private let nonUTF8Data = Data(bytes: [0x80]) + class DatabaseValueConversionTests : GRDBTestCase { private func assertDecoding( @@ -222,10 +226,10 @@ class DatabaseValueConversionTests : GRDBTestCase { return .rollback } - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + // emojiString is turned to Text try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) + try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [emojiString]) let sql = "SELECT textAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -233,15 +237,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) + return .rollback + } + + // emojiData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [emojiData]) + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) return .rollback } - // Blob is turned to Blob + // nonUTF8Data is turned to Blob try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) + try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [nonUTF8Data]) let sql = "SELECT textAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -249,8 +269,8 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) +// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } } @@ -448,10 +468,10 @@ class DatabaseValueConversionTests : GRDBTestCase { return .rollback } - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + // emojiString is turned to Text try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) + try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [emojiString]) let sql = "SELECT realAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -459,15 +479,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) + return .rollback + } + + // emojiData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [emojiData]) + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) return .rollback } - // Blob is turned to Blob + // nonUTF8Data is turned to Blob try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) + try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [nonUTF8Data]) let sql = "SELECT realAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -475,8 +511,8 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) +// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } } @@ -602,10 +638,10 @@ class DatabaseValueConversionTests : GRDBTestCase { return .rollback } - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + // emojiString is turned to Text try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [emojiString]) let sql = "SELECT noneAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -613,15 +649,15 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) return .rollback } - // Blob is turned to Blob + // emojiData is turned to Blob try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [emojiData]) let sql = "SELECT noneAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -629,8 +665,24 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) + return .rollback + } + + // nonUTF8Data is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [nonUTF8Data]) + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) +// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } } @@ -802,10 +854,10 @@ class DatabaseValueConversionTests : GRDBTestCase { return .rollback } - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + // emojiString is turned to Text try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'"]) + try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [emojiString]) let sql = "SELECT \(columnName) FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .text) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -813,15 +865,31 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) + return .rollback + } + + // emojiData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [emojiData]) + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: emojiString, expectedDatabaseValueConversion: emojiString) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: emojiData, expectedDatabaseValueConversion: emojiData) return .rollback } - // Blob is turned to Blob + // nonUTF8Data is turned to Blob try dbQueue.inTransaction { db in - try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: ["'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)]) + try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [nonUTF8Data]) let sql = "SELECT \(columnName) FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) @@ -829,8 +897,8 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'", expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'") - try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8), expectedDatabaseValueConversion: "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) +// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } } From 0b77d0d7453e8f2bf04c8cdcabb6c0545ae8d623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 16:10:51 +0200 Subject: [PATCH 24/37] Better feedback for conversion errors from StatementColumnConvertible --- GRDB/Core/DatabaseValue.swift | 4 ++++ GRDB/Core/DatabaseValueConversion.swift | 10 ++++++++++ .../Foundation/DatabaseDateComponents.swift | 4 ++-- GRDB/Core/Support/Foundation/Date.swift | 6 ++---- .../StandardLibrary/StandardLibrary.swift | 20 ++++++++++--------- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index acd23942ec..f505c8d6ef 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -105,6 +105,8 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon case SQLITE_FLOAT: storage = .double(sqlite3_value_double(sqliteValue)) case SQLITE_TEXT: + // Builds an invalid string when decoding a blob that contains + // invalid UTF8 data. storage = .string(String(cString: sqlite3_value_text(sqliteValue)!)) case SQLITE_BLOB: if let bytes = sqlite3_value_blob(sqliteValue) { @@ -129,6 +131,8 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon case SQLITE_FLOAT: storage = .double(sqlite3_column_double(sqliteStatement, Int32(index))) case SQLITE_TEXT: + // Builds an invalid string when decoding a blob that contains + // invalid UTF8 data. storage = .string(String(cString: sqlite3_column_text(sqliteStatement, Int32(index)))) case SQLITE_BLOB: if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 2874c85586..a83b9308af 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -131,6 +131,16 @@ func fatalConversionError(to: T.Type, from dbValue: DatabaseValue?, conversio fatalError(conversionErrorMessage(to: T.self, from: dbValue, conversionContext: conversionContext), file: file, line: line) } +func fatalConversionError(to: T.Type, sqliteStatement: SQLiteStatement, index: Int32) -> Never { + let sql = String(cString: sqlite3_sql(sqliteStatement)) + .trimmingCharacters(in: statementSeparatorCharacterSet) + + fatalConversionError( + to: T.self, + from: DatabaseValue(sqliteStatement: sqliteStatement, index: index), + conversionContext: ValueConversionContext(sql: sql, arguments: nil)) +} + // MARK: - DatabaseValueConvertible /// Lossless conversions from database values and rows diff --git a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift index 92e0b92887..8c8b0776a0 100644 --- a/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift +++ b/GRDB/Core/Support/Foundation/DatabaseDateComponents.swift @@ -76,14 +76,14 @@ public struct DatabaseDateComponents : DatabaseValueConvertible, StatementColumn /// - index: The column index. public init(sqliteStatement: SQLiteStatement, index: Int32) { guard let cString = sqlite3_column_text(sqliteStatement, index) else { - fatalError("could not convert database value \(DatabaseValue(sqliteStatement: sqliteStatement, index: index)) to DatabaseDateComponents") + fatalConversionError(to: DatabaseDateComponents.self, sqliteStatement: sqliteStatement, index: index) } let length = Int(sqlite3_column_bytes(sqliteStatement, index)) // avoid an strlen let optionalComponents = cString.withMemoryRebound(to: Int8.self, capacity: length + 1 /* trailing \0 */) { cString in SQLiteDateParser().components(cString: cString, length: length) } guard let components = optionalComponents else { - fatalError("could not convert database value \(String(cString: cString)) to DatabaseDateComponents") + fatalConversionError(to: DatabaseDateComponents.self, sqliteStatement: sqliteStatement, index: index) } self.dateComponents = components.dateComponents self.format = components.format diff --git a/GRDB/Core/Support/Foundation/Date.swift b/GRDB/Core/Support/Foundation/Date.swift index c5e1e1d6b1..2d8ea3bc7b 100644 --- a/GRDB/Core/Support/Foundation/Date.swift +++ b/GRDB/Core/Support/Foundation/Date.swift @@ -129,13 +129,11 @@ extension Date: StatementColumnConvertible { case SQLITE_TEXT: let databaseDateComponents = DatabaseDateComponents(sqliteStatement: sqliteStatement, index: index) guard let date = Date(databaseDateComponents: databaseDateComponents) else { - let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) - fatalError("could not convert database value \(dbValue) to Date") + fatalConversionError(to: Date.self, sqliteStatement: sqliteStatement, index: index) } self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate) default: - let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index) - fatalError("could not convert database value \(dbValue) to Date") + fatalConversionError(to: Date.self, sqliteStatement: sqliteStatement, index: index) } } } diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index 4397ffe7eb..8e92e53d83 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -105,7 +105,7 @@ extension Int: DatabaseValueConvertible, StatementColumnConvertible { if let v = Int(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to Int") + fatalConversionError(to: Int.self, sqliteStatement: sqliteStatement, index: index) } } @@ -133,7 +133,7 @@ extension Int8: DatabaseValueConvertible, StatementColumnConvertible { if let v = Int8(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to Int8") + fatalConversionError(to: Int8.self, sqliteStatement: sqliteStatement, index: index) } } @@ -161,7 +161,7 @@ extension Int16: DatabaseValueConvertible, StatementColumnConvertible { if let v = Int16(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to Int16") + fatalConversionError(to: Int16.self, sqliteStatement: sqliteStatement, index: index) } } @@ -189,7 +189,7 @@ extension Int32: DatabaseValueConvertible, StatementColumnConvertible { if let v = Int32(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to Int32") + fatalConversionError(to: Int32.self, sqliteStatement: sqliteStatement, index: index) } } @@ -249,7 +249,7 @@ extension UInt: DatabaseValueConvertible, StatementColumnConvertible { if let v = UInt(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to UInt") + fatalConversionError(to: UInt.self, sqliteStatement: sqliteStatement, index: index) } } @@ -277,7 +277,7 @@ extension UInt8: DatabaseValueConvertible, StatementColumnConvertible { if let v = UInt8(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to UInt8") + fatalConversionError(to: UInt8.self, sqliteStatement: sqliteStatement, index: index) } } @@ -305,7 +305,7 @@ extension UInt16: DatabaseValueConvertible, StatementColumnConvertible { if let v = UInt16(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to UInt16") + fatalConversionError(to: UInt16.self, sqliteStatement: sqliteStatement, index: index) } } @@ -333,7 +333,7 @@ extension UInt32: DatabaseValueConvertible, StatementColumnConvertible { if let v = UInt32(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to UInt32") + fatalConversionError(to: UInt32.self, sqliteStatement: sqliteStatement, index: index) } } @@ -361,7 +361,7 @@ extension UInt64: DatabaseValueConvertible, StatementColumnConvertible { if let v = UInt64(exactly: int64) { self = v } else { - fatalError("could not convert database value \(int64) to UInt64") + fatalConversionError(to: UInt64.self, sqliteStatement: sqliteStatement, index: index) } } @@ -445,6 +445,8 @@ extension String: DatabaseValueConvertible, StatementColumnConvertible { /// - sqliteStatement: A pointer to an SQLite statement. /// - index: The column index. public init(sqliteStatement: SQLiteStatement, index: Int32) { + // Builds an invalid string when decoding a blob that contains + // invalid UTF8 data. self = String(cString: sqlite3_column_text(sqliteStatement, Int32(index))!) } From e8dacb31ccef192c579d10e1280a400145aae45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 16:22:27 +0200 Subject: [PATCH 25/37] Tests for failed value conversions --- .../DatabaseValueConversionTests.swift | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index 0ced418404..db666e4a28 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -78,7 +78,8 @@ class DatabaseValueConversionTests : GRDBTestCase { do { // test T.fromDatabaseValue - let dbValueConversion = try T.fromDatabaseValue(DatabaseValue.fetchOne(db, sql)!) + let dbValue = try DatabaseValue.fetchOne(db, sql)! + let dbValueConversion = T.fromDatabaseValue(dbValue) XCTAssert( dbValueConversion == expectedDatabaseValueConversion, "unexpected SQLite conversion: \(stringRepresentation(dbValueConversion)) instead of \(stringRepresentation(expectedDatabaseValueConversion))", @@ -86,6 +87,20 @@ class DatabaseValueConversionTests : GRDBTestCase { } } + private func assertFailedDecoding( + _ db: Database, + _ sql: String, + _ type: T.Type, + file: StaticString = #file, + line: UInt = #line) throws + { + // We can only test failed decoding from database value, since + // StatementColumnConvertible only supports optimistic decoding which + // never fails. + let dbValue = try DatabaseValue.fetchOne(db, sql)! + XCTAssertNil(T.fromDatabaseValue(dbValue), file: file, line: line) + } + // Datatypes In SQLite Version 3: https://www.sqlite.org/datatype3.html override func setup(_ dbWriter: DatabaseWriter) throws { @@ -269,7 +284,9 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // FIXME? low-level StatementColumnConvertible currently decodes an invalid string // try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertFailedDecoding(db, sql, String.self) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } @@ -411,9 +428,9 @@ class DatabaseValueConversionTests : GRDBTestCase { let sql = "SELECT realAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) -// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertFailedDecoding(db, sql, Int.self) + try assertFailedDecoding(db, sql, Int32.self) + try assertFailedDecoding(db, sql, Int64.self) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) @@ -459,9 +476,9 @@ class DatabaseValueConversionTests : GRDBTestCase { let sql = "SELECT realAffinity FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) -// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertFailedDecoding(db, sql, Int.self) + try assertFailedDecoding(db, sql, Int32.self) + try assertFailedDecoding(db, sql, Int64.self) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) @@ -511,7 +528,9 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // FIXME? low-level StatementColumnConvertible currently decodes an invalid string // try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertFailedDecoding(db, sql, String.self) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } @@ -681,7 +700,9 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // FIXME? low-level StatementColumnConvertible currently decodes an invalid string // try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertFailedDecoding(db, sql, String.self) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } @@ -797,9 +818,9 @@ class DatabaseValueConversionTests : GRDBTestCase { let sql = "SELECT \(columnName) FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) -// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertFailedDecoding(db, sql, Int.self) + try assertFailedDecoding(db, sql, Int32.self) + try assertFailedDecoding(db, sql, Int64.self) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) @@ -845,9 +866,9 @@ class DatabaseValueConversionTests : GRDBTestCase { let sql = "SELECT \(columnName) FROM `values`" XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .real) try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: true, expectedDatabaseValueConversion: true) -// try assertDecoding(db, sql, Int.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) -// try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) + try assertFailedDecoding(db, sql, Int.self) + try assertFailedDecoding(db, sql, Int32.self) + try assertFailedDecoding(db, sql, Int64.self) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 1e20, expectedDatabaseValueConversion: 1e20) try assertDecoding(db, sql, String.self, expectedSQLiteConversion: "1.0e+20", expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: "1.0e+20".data(using: .utf8), expectedDatabaseValueConversion: nil) @@ -897,7 +918,9 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // FIXME? low-level StatementColumnConvertible currently decodes an invalid string // try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) + try assertFailedDecoding(db, sql, String.self) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } From b47f5864ac20ca93a733cf09d91eed5f8657b8a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 16:49:07 +0200 Subject: [PATCH 26/37] Even better feedback for conversion errors from StatementColumnConvertible --- GRDB/Core/DatabaseValueConversion.swift | 13 +++-- GRDB/Core/Row.swift | 32 ++++++++++++ .../DatabaseValueConversionErrorTests.swift | 50 ++++++++++++++++++- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index a83b9308af..b635777765 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -67,6 +67,13 @@ extension ValueConversionContext { sql: statement.sql, arguments: statement.arguments, column: nil) + } else if let sqliteStatement = row.sqliteStatement { + let sql = String(cString: sqlite3_sql(sqliteStatement)).trimmingCharacters(in: statementSeparatorCharacterSet) + self.init( + row: row.copy(), + sql: sql, + arguments: nil, + column: nil) } else { self.init( row: row.copy(), @@ -132,13 +139,11 @@ func fatalConversionError(to: T.Type, from dbValue: DatabaseValue?, conversio } func fatalConversionError(to: T.Type, sqliteStatement: SQLiteStatement, index: Int32) -> Never { - let sql = String(cString: sqlite3_sql(sqliteStatement)) - .trimmingCharacters(in: statementSeparatorCharacterSet) - + let row = Row(sqliteStatement: sqliteStatement) fatalConversionError( to: T.self, from: DatabaseValue(sqliteStatement: sqliteStatement, index: index), - conversionContext: ValueConversionContext(sql: sql, arguments: nil)) + conversionContext: ValueConversionContext(row).atColumn(Int(index))) } // MARK: - DatabaseValueConvertible diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 046e7883ac..c393dc9301 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -94,6 +94,15 @@ public final class Row : Equatable, Hashable, RandomAccessCollection, Expressibl self.count = Int(sqlite3_column_count(sqliteStatement)) } + /// Creates a row that maps an SQLite statement. Further calls to + /// sqlite3_step() modify the row. + init(sqliteStatement: SQLiteStatement) { + self.sqliteStatement = sqliteStatement + self.statementRef = nil + self.impl = SQLiteStatementRowImpl(sqliteStatement: sqliteStatement) + self.count = Int(sqlite3_column_count(sqliteStatement)) + } + /// Creates a row that contain a copy of the current state of the /// SQLite statement. Further calls to sqlite3_step() do not modify the row. /// @@ -1407,6 +1416,29 @@ private struct StatementRowImpl : RowImpl { } } +// This one is not optimized at all, since it is only used in fatal conversion errors, so far +private struct SQLiteStatementRowImpl : RowImpl { + let sqliteStatement: SQLiteStatement + var count: Int { return Int(sqlite3_column_count(sqliteStatement)) } + var isFetched: Bool { return true } + + func columnName(atUncheckedIndex index: Int) -> String { + return String(cString: sqlite3_column_name(sqliteStatement, Int32(index))) + } + + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { + return DatabaseValue(sqliteStatement: sqliteStatement, index: Int32(index)) + } + + func index(ofColumn name: String) -> Int? { + let name = name.lowercased() + for index in 0.. Date: Wed, 15 Aug 2018 16:55:12 +0200 Subject: [PATCH 27/37] Oh men of little faith! --- GRDB/Core/DatabaseValue.swift | 4 ---- .../StandardLibrary/StandardLibrary.swift | 2 -- .../DatabaseValueConversionTests.swift | 17 +++++------------ 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index f505c8d6ef..acd23942ec 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -105,8 +105,6 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon case SQLITE_FLOAT: storage = .double(sqlite3_value_double(sqliteValue)) case SQLITE_TEXT: - // Builds an invalid string when decoding a blob that contains - // invalid UTF8 data. storage = .string(String(cString: sqlite3_value_text(sqliteValue)!)) case SQLITE_BLOB: if let bytes = sqlite3_value_blob(sqliteValue) { @@ -131,8 +129,6 @@ public struct DatabaseValue: Hashable, CustomStringConvertible, DatabaseValueCon case SQLITE_FLOAT: storage = .double(sqlite3_column_double(sqliteStatement, Int32(index))) case SQLITE_TEXT: - // Builds an invalid string when decoding a blob that contains - // invalid UTF8 data. storage = .string(String(cString: sqlite3_column_text(sqliteStatement, Int32(index)))) case SQLITE_BLOB: if let bytes = sqlite3_column_blob(sqliteStatement, Int32(index)) { diff --git a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift index 8e92e53d83..976ae455b5 100644 --- a/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift +++ b/GRDB/Core/Support/StandardLibrary/StandardLibrary.swift @@ -445,8 +445,6 @@ extension String: DatabaseValueConvertible, StatementColumnConvertible { /// - sqliteStatement: A pointer to an SQLite statement. /// - index: The column index. public init(sqliteStatement: SQLiteStatement, index: Int32) { - // Builds an invalid string when decoding a blob that contains - // invalid UTF8 data. self = String(cString: sqlite3_column_text(sqliteStatement, Int32(index))!) } diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index db666e4a28..5d7f8be458 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -32,6 +32,7 @@ private extension DatabaseValue { private let emojiString = "'fooéı👨👨🏿🇫🇷🇨🇮'" private let emojiData = emojiString.data(using: .utf8) private let nonUTF8Data = Data(bytes: [0x80]) +private let invalidString = "\u{FFFD}" // decoded from nonUTF8Data class DatabaseValueConversionTests : GRDBTestCase { @@ -284,9 +285,7 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - // FIXME? low-level StatementColumnConvertible currently decodes an invalid string -// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) - try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: invalidString, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } @@ -528,9 +527,7 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - // FIXME? low-level StatementColumnConvertible currently decodes an invalid string -// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) - try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: invalidString, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } @@ -700,9 +697,7 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - // FIXME? low-level StatementColumnConvertible currently decodes an invalid string -// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) - try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: invalidString, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } @@ -918,9 +913,7 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) - // FIXME? low-level StatementColumnConvertible currently decodes an invalid string -// try assertDecoding(db, sql, String.self, expectedSQLiteConversion: nil, expectedDatabaseValueConversion: nil) - try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, String.self, expectedSQLiteConversion: invalidString, expectedDatabaseValueConversion: nil) try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } From 4d343e893e519b9d8f8c97ecd7be14f5915c1cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 17:09:15 +0200 Subject: [PATCH 28/37] More tests for Data -> String conversion: play with JPEG :-) --- .../DatabaseValueConversionTests.swift | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index 5d7f8be458..fc0ae3a832 100644 --- a/Tests/GRDBTests/DatabaseValueConversionTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionTests.swift @@ -33,6 +33,10 @@ private let emojiString = "'fooéı👨👨🏿🇫🇷🇨🇮'" private let emojiData = emojiString.data(using: .utf8) private let nonUTF8Data = Data(bytes: [0x80]) private let invalidString = "\u{FFFD}" // decoded from nonUTF8Data +// Until SPM tests can load resources, disable this test for SPM. +#if !SWIFT_PACKAGE +private let jpegData = try! Data(contentsOf: Bundle(for: DatabaseValueConversionTests.self).url(forResource: "Betty", withExtension: "jpeg")!) +#endif class DatabaseValueConversionTests : GRDBTestCase { @@ -53,6 +57,9 @@ class DatabaseValueConversionTests : GRDBTestCase { do { // test T.fetchOne let sqliteConversion = try T.fetchOne(db, sql) + if let s = sqliteConversion as? String { + print(s.utf8.map { $0 }) + } XCTAssert( sqliteConversion == expectedSQLiteConversion, "unexpected SQLite conversion: \(stringRepresentation(sqliteConversion)) instead of \(stringRepresentation(expectedSQLiteConversion))", @@ -289,6 +296,25 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } + + #if !SWIFT_PACKAGE + // jpegData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (textAffinity) VALUES (?)", arguments: [jpegData]) + let sql = "SELECT textAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // TODO: test SQLite decoding to String + try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: jpegData, expectedDatabaseValueConversion: jpegData) + return .rollback + } + #endif } func testNumericAffinity() throws { @@ -531,6 +557,25 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } + + #if !SWIFT_PACKAGE + // jpegData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (realAffinity) VALUES (?)", arguments: [jpegData]) + let sql = "SELECT realAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // TODO: test SQLite decoding to String + try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: jpegData, expectedDatabaseValueConversion: jpegData) + return .rollback + } + #endif } func testNoneAffinity() throws { @@ -701,6 +746,25 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } + + #if !SWIFT_PACKAGE + // jpegData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (noneAffinity) VALUES (?)", arguments: [jpegData]) + let sql = "SELECT noneAffinity FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // TODO: test SQLite decoding to String + try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: jpegData, expectedDatabaseValueConversion: jpegData) + return .rollback + } + #endif } func testNumericAffinity(_ columnName: String) throws { @@ -917,5 +981,24 @@ class DatabaseValueConversionTests : GRDBTestCase { try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } + + #if !SWIFT_PACKAGE + // jpegData is turned to Blob + + try dbQueue.inTransaction { db in + try db.execute("INSERT INTO `values` (\(columnName)) VALUES (?)", arguments: [jpegData]) + let sql = "SELECT \(columnName) FROM `values`" + XCTAssertEqual(try DatabaseValue.fetchOne(db, sql)!.storageClass, .blob) + try assertDecoding(db, sql, Bool.self, expectedSQLiteConversion: false, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int.self, expectedSQLiteConversion:0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int32.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Int64.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Double.self, expectedSQLiteConversion: 0, expectedDatabaseValueConversion: nil) + // TODO: test SQLite decoding to String + try assertFailedDecoding(db, sql, String.self) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: jpegData, expectedDatabaseValueConversion: jpegData) + return .rollback + } + #endif } } From d00421abe0e1ac921390cbb1eb8ffbd16eaaaeeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 17:17:08 +0200 Subject: [PATCH 29/37] Restore SPM: add missing Foundation import --- GRDB/Record/PersistableRecord+Encodable.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index bb38d1d0a8..e40c966a77 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -1,3 +1,5 @@ +import Foundation + private struct PersistableRecordKeyedEncodingContainer : KeyedEncodingContainerProtocol { let encode: DatabaseValuePersistenceEncoder From 659db4fdd957752a6246689b5b791675f7adc0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 17:33:05 +0200 Subject: [PATCH 30/37] Safer default implementation for RowImpl.copiedRow(_:) --- GRDB/Core/Row.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index c393dc9301..7dd33e89d8 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -1223,8 +1223,8 @@ protocol RowImpl { extension RowImpl { func copiedRow(_ row: Row) -> Row { - // unless customized, assume immutable row (see StatementRowImpl and AdaptedRowImpl for customization) - return row + // unless customized, assume unsafe and unadapted row + return Row(impl: ArrayRowImpl(columns: row.map { $0 })) } func unscopedRow(_ row: Row) -> Row { @@ -1300,6 +1300,10 @@ private struct ArrayRowImpl : RowImpl { let lowercaseName = name.lowercased() return columns.index { (column, _) in column.lowercased() == lowercaseName } } + + func copiedRow(_ row: Row) -> Row { + return row + } } @@ -1335,6 +1339,10 @@ private struct StatementCopyRowImpl : RowImpl { let lowercaseName = name.lowercased() return columnNames.index { $0.lowercased() == lowercaseName } } + + func copiedRow(_ row: Row) -> Row { + return row + } } @@ -1462,4 +1470,8 @@ private struct EmptyRowImpl : RowImpl { func index(ofColumn name: String) -> Int? { return nil } + + func copiedRow(_ row: Row) -> Row { + return row + } } From 2eb09554202483a63473b2d8779ee220252e8bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 18:34:47 +0200 Subject: [PATCH 31/37] JSON encoding: choose strategies Data: .base64 Date: .millisecondsSince1970 NonConformingFloat: .throw --- GRDB/Record/FetchableRecord+Decodable.swift | 12 +++++++-- GRDB/Record/PersistableRecord+Encodable.swift | 26 ++++++++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 29b19e29ec..98530e4148 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -92,7 +92,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer guard let data = row.dataNoCopy(atIndex: index) else { fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) } - return try JSONDecoder().decode(type.self, from: data) + return try makeJSONDecoder().decode(type.self, from: data) } } } @@ -142,7 +142,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer guard let data = row.dataNoCopy(atIndex: index) else { fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) } - return try JSONDecoder().decode(type.self, from: data) + return try makeJSONDecoder().decode(type.self, from: data) } } } @@ -303,6 +303,14 @@ private struct RowSingleValueDecoder: Decoder { /// The error that triggers JSON decoding private struct JSONRequiredError: Error { } +func makeJSONDecoder() -> JSONDecoder { + let encoder = JSONDecoder() + encoder.dataDecodingStrategy = .base64 + encoder.dateDecodingStrategy = .millisecondsSince1970 + encoder.nonConformingFloatDecodingStrategy = .throw + return encoder +} + extension FetchableRecord where Self: Decodable { /// Initializes a record from `row`. public init(row: Row) { diff --git a/GRDB/Record/PersistableRecord+Encodable.swift b/GRDB/Record/PersistableRecord+Encodable.swift index e40c966a77..ac26dbdf01 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -50,12 +50,7 @@ private struct PersistableRecordKeyedEncodingContainer : KeyedEn try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) } catch is JSONRequiredError { // Encode to JSON - let encoder = JSONEncoder() - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - // guarantee some stability in order to ease record comparison - encoder.outputFormatting = .sortedKeys - } - let jsonData = try encoder.encode(value) + let jsonData = try makeJSONEncoder().encode(value) // Store JSON String in the database for easier debugging and // database inspection. Thanks to SQLite weak typing, we won't @@ -176,12 +171,7 @@ private struct DatabaseValueEncodingContainer : SingleValueEncodingContainer { try value.encode(to: DatabaseValueEncoder(key: key, encode: encode)) } catch is JSONRequiredError { // Encode to JSON - let encoder = JSONEncoder() - if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { - // guarantee some stability in order to ease record comparison - encoder.outputFormatting = .sortedKeys - } - let jsonData = try encoder.encode(value) + let jsonData = try makeJSONEncoder().encode(value) // Store JSON String in the database for easier debugging and // database inspection. Thanks to SQLite weak typing, we won't @@ -365,6 +355,18 @@ private struct JSONRequiredError: Error { } private typealias DatabaseValuePersistenceEncoder = (_ value: DatabaseValueConvertible?, _ key: String) -> Void +func makeJSONEncoder() -> JSONEncoder { + let encoder = JSONEncoder() + encoder.dataEncodingStrategy = .base64 + encoder.dateEncodingStrategy = .millisecondsSince1970 + encoder.nonConformingFloatEncodingStrategy = .throw + if #available(watchOS 4.0, OSX 10.13, iOS 11.0, *) { + // guarantee some stability in order to ease record comparison + encoder.outputFormatting = .sortedKeys + } + return encoder +} + extension MutablePersistableRecord where Self: Encodable { public func encode(to container: inout PersistenceContainer) { // The inout container parameter won't enter an escaping closure since From 9bb4ff86f1d5d9c102390b153f4359b9b95c94b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 18:35:06 +0200 Subject: [PATCH 32/37] Tests for JSON date strategy --- .../FetchableRecordDecodableTests.swift | 28 ++++++++++++++++-- ...tablePersistableRecordEncodableTests.swift | 29 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index ac233da93d..3efe636131 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -697,6 +697,30 @@ extension FetchableRecordDecodableTests { } } } - - + + func testJSONDateEncodingStrategy() throws { + struct Record: FetchableRecord, Decodable { + let date: Date + let optionalDate: Date? + let dates: [Date] + let optionalDates: [Date?] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let record = try Record.fetchOne(db, "SELECT ? AS date, ? AS optionalDate, ? AS dates, ? AS optionalDates", arguments: [ + "1970-01-01 00:02:08.000", + "1970-01-01 00:02:08.000", + "[128000]", + "[null,128000]" + ])! + XCTAssertEqual(record.date.timeIntervalSince1970, 128) + XCTAssertEqual(record.optionalDate!.timeIntervalSince1970, 128) + XCTAssertEqual(record.dates.count, 1) + XCTAssertEqual(record.dates[0].timeIntervalSince1970, 128) + XCTAssertEqual(record.optionalDates.count, 2) + XCTAssertNil(record.optionalDates[0]) + XCTAssertEqual(record.optionalDates[1]!.timeIntervalSince1970, 128) + } + } } diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index 70f238d7a9..82b016e145 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -415,4 +415,33 @@ extension MutablePersistableRecordEncodableTests { XCTAssertEqual(fetchedUUID, value.uuid) } } + + func testJSONDateEncodingStrategy() throws { + struct Record: PersistableRecord, Encodable { + let date: Date + let optionalDate: Date? + let dates: [Date] + let optionalDates: [Date?] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "record") { t in + t.column("date", .text) + t.column("optionalDate", .text) + t.column("dates", .text) + t.column("optionalDates", .text) + } + + let date = Date(timeIntervalSince1970: 128) + let record = Record(date: date, optionalDate: date, dates: [date], optionalDates: [nil, date]) + try record.insert(db) + + let row = try Row.fetchOne(db, Record.all())! + XCTAssertEqual(row["date"], "1970-01-01 00:02:08.000") + XCTAssertEqual(row["optionalDate"], "1970-01-01 00:02:08.000") + XCTAssertEqual(row["dates"], "[128000]") + XCTAssertEqual(row["optionalDates"], "[null,128000]") + } + } } From 02db2a8dc64281e382615081fb7f90b5884679b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 18:40:24 +0200 Subject: [PATCH 33/37] Tests for JSON data strategy --- .../FetchableRecordDecodableTests.swift | 29 ++++++++++++++++++- ...tablePersistableRecordEncodableTests.swift | 29 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 3efe636131..c5bae1bbf6 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -698,6 +698,33 @@ extension FetchableRecordDecodableTests { } } + func testJSONDataEncodingStrategy() throws { + struct Record: FetchableRecord, Decodable { + let data: Data + let optionalData: Data? + let datas: [Data] + let optionalDatas: [Data?] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let data = "foo".data(using: .utf8)! + let record = try Record.fetchOne(db, "SELECT ? AS data, ? AS optionalData, ? AS datas, ? AS optionalDatas", arguments: [ + data, + data, + "[\"Zm9v\"]", + "[null, \"Zm9v\"]" + ])! + XCTAssertEqual(record.data, data) + XCTAssertEqual(record.optionalData!, data) + XCTAssertEqual(record.datas.count, 1) + XCTAssertEqual(record.datas[0], data) + XCTAssertEqual(record.optionalDatas.count, 2) + XCTAssertNil(record.optionalDatas[0]) + XCTAssertEqual(record.optionalDatas[1]!, data) + } + } + func testJSONDateEncodingStrategy() throws { struct Record: FetchableRecord, Decodable { let date: Date @@ -713,7 +740,7 @@ extension FetchableRecordDecodableTests { "1970-01-01 00:02:08.000", "[128000]", "[null,128000]" - ])! + ])! XCTAssertEqual(record.date.timeIntervalSince1970, 128) XCTAssertEqual(record.optionalDate!.timeIntervalSince1970, 128) XCTAssertEqual(record.dates.count, 1) diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index 82b016e145..fd340eda9f 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -416,6 +416,35 @@ extension MutablePersistableRecordEncodableTests { } } + func testJSONDataEncodingStrategy() throws { + struct Record: PersistableRecord, Encodable { + let data: Data + let optionalData: Data? + let datas: [Data] + let optionalDatas: [Data?] + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + try db.create(table: "record") { t in + t.column("data", .text) + t.column("optionalData", .text) + t.column("datas", .text) + t.column("optionalDatas", .text) + } + + let data = "foo".data(using: .utf8)! + let record = Record(data: data, optionalData: data, datas: [data], optionalDatas: [nil, data]) + try record.insert(db) + + let row = try Row.fetchOne(db, Record.all())! + XCTAssertEqual(row["data"], data) + XCTAssertEqual(row["optionalData"], data) + XCTAssertEqual(row["datas"], "[\"Zm9v\"]") + XCTAssertEqual(row["optionalDatas"], "[null,\"Zm9v\"]") + } + } + func testJSONDateEncodingStrategy() throws { struct Record: PersistableRecord, Encodable { let date: Date From dfb9421bd7e8aa3fcebcc4b05b069398480c9387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 18:53:10 +0200 Subject: [PATCH 34/37] Documentation for JSON support --- README.md | 92 ++++++++++++++++--------------------------------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 97921344c5..517b338f33 100644 --- a/README.md +++ b/README.md @@ -2567,25 +2567,15 @@ struct Link : PersistableRecord { ## Codable Records -[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4, GRDB supports all Codable conforming types +[Swift Archival & Serialization](https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md) was introduced with Swift 4. GRDB provides default implementations for [`FetchableRecord.init(row:)`](#fetchablerecord-protocol) and [`PersistableRecord.encode(to:)`](#persistablerecord-protocol) for record types that also adopt an archival protocol (`Codable`, `Encodable` or `Decodable`). When all their properties are themselves codable, Swift generates the archiving methods, and you don't need to write them down: ```swift -// Declare a Codable struct or class, nested Codable objects as well as Sets, Arrays, Dictionaries and Optionals are all supported +// Declare a Codable struct or class... struct Player: Codable { - let name: String - let score: Int - let scores: [Int] - let lastMedal: PlayerMedal - let medals: [PlayerMedal] - let timeline: [String: PlayerMedal] - } - -// A simple Codable that will be nested in a parent Codable -struct PlayerMedal : Codable { - let name: String - let type: String + var name: String + var score: Int } // Adopt Record protocols... @@ -2598,69 +2588,41 @@ try dbQueue.write { db in } ``` -GRDB support for Codable works with standard library types like String, Int, and Double; and Foundation types like Date, Data, and URL, Apple lists conformance for : (Array, CGAffineTransform, CGPoint, CGRect, CGSize, CGVector, DateInterval, Decimal, Dictionary, MPMusicPlayerPlayParameters, Optional, Set) Anything else like say a CLLocationCoordinate2D will break conformity e.g.: +When a record contains a codable property that is not a simple [value](#values) (Bool, Int, String, Date, Swift enums, etc.), that value is encoded and decoded as a **JSON string**. For example: ```swift -struct Place: FetchableRecord, PersistableRecord, Codable { - var title: String - var coordinate: CLLocationCoordinate2D // <- Does not conform to Codable +enum AchievementColor: String, Codable { + case bronze, silver, gold } -``` -Make it flat, as below, and you'll be granted with all Codable and GRDB advantages: - -```swift -struct Place: Codable { - // Stored properties are plain values: - var title: String - var latitude: CLLocationDegrees - var longitude: CLLocationDegrees - - // Complex property is computed: - var coordinate: CLLocationCoordinate2D { - get { - return CLLocationCoordinate2D( - latitude: latitude, - longitude: longitude) - } - set { - latitude = newValue.latitude - longitude = newValue.longitude - } - } +struct Achievement: Codable { + var name: String + var color: AchievementColor } -// Free database support! -extension Place: FetchableRecord, PersistableRecord { } -``` - -GRDB ships with support for nested codable records, but this is a more complex topic. See [Associations](Documentation/AssociationsBasics.md) for more information. - -As documented with the [PersistableRecord] protocol, have your struct records use MutablePersistableRecord instead of PersistableRecord when they store their automatically incremented row id: - -```swift -struct Place: Codable { - var id: Int64? // <- the row id - var title: String - var latitude: CLLocationDegrees - var longitude: CLLocationDegrees - var coordinate: CLLocationCoordinate2D { ... } +struct Player: Codable, FetchableRecord, PersistableRecord { + var name: String + var score: Int + var achievements: [Achievement] } -extension Place: FetchableRecord, MutablePersistableRecord { - mutating func didInsert(with rowID: Int64, for column: String?) { - // Update id after insertion - id = rowID - } +try dbQueue.write { db in + // INSERT INTO player (name, score, achievements) + // VALUES ( + // 'Arthur', + // 100, + // '[{"color":"gold","name":"Use Codable Records"}]') + let achievement = Achievement(name: "Use Codable Records", color: .gold) + try Player(name: "Arthur", score: 100, achievements: [achievement]).insert(db) } - -var place = Place(id: nil, ...) -try place.insert(db) -place.id // A unique id ``` +> :point_up: **Note**: Some codable values have a different way to encode and decode themselves in a standard archive vs. a database column. For example, [Date](#date-and-datecomponents) saves itself as a numerical timestamp (archive) or a string (database). When such an ambiguity happens, GRDB always favors customized database encoding and decoding. + +> :point_up: **Note about JSON support**: GRDB uses the standard **[JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder) and [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) from Foundation. Data values are handled with the `.base64` strategy, Date with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy. Check Foundation documentation for more information. + +> :point_up: **Note about JSON support**: JSON encoding uses the `.sortedKeys` option when available (iOS 11.0+, macOS 10.13+, watchOS 4.0+). In previous operating system versions, the ordering of JSON keys may be unstable, and this may negatively impact [Record Comparison]. -> :point_up: **Note**: Some values have a different way to encode and decode themselves in a standard archive vs. the database. For example, [Date](#date-and-datecomponents) saves itself as a numerical timestamp (archive) or a string (database). When such an ambiguity happens, GRDB always favors customized database encoding and decoding. ## Record Class From 2b8c8389fef81c090a4240662010b6bc43b72de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 19:01:11 +0200 Subject: [PATCH 35/37] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 517b338f33..34f76ef4fd 100644 --- a/README.md +++ b/README.md @@ -2619,7 +2619,7 @@ try dbQueue.write { db in > :point_up: **Note**: Some codable values have a different way to encode and decode themselves in a standard archive vs. a database column. For example, [Date](#date-and-datecomponents) saves itself as a numerical timestamp (archive) or a string (database). When such an ambiguity happens, GRDB always favors customized database encoding and decoding. -> :point_up: **Note about JSON support**: GRDB uses the standard **[JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder) and [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) from Foundation. Data values are handled with the `.base64` strategy, Date with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy. Check Foundation documentation for more information. +> :point_up: **Note about JSON support**: GRDB uses the standard [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder) and [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) from Foundation. Data values are handled with the `.base64` strategy, Date with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy. Check Foundation documentation for more information. > :point_up: **Note about JSON support**: JSON encoding uses the `.sortedKeys` option when available (iOS 11.0+, macOS 10.13+, watchOS 4.0+). In previous operating system versions, the ordering of JSON keys may be unstable, and this may negatively impact [Record Comparison]. From 44b998c08f5c351d7171d0c49b425a399f7ee3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 19:38:03 +0200 Subject: [PATCH 36/37] CHANGELOG.md --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7237c0590f..e619f632d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,14 @@ Release Notes ## Next Version -- Cursors of optimized values (Strint, Int, Date, etc.) have been renamed: use FastDatabaseValueCursor and FastNullableDatabaseValueCursor instead of the deprecated ColumnCursor and NullableColumnCursor. -- [#384](https://github.com/groue/GRDB.swift/pull/384): Improve database value decoding diagnostics +- [#397](https://github.com/groue/GRDB.swift/pull/397): JSON encoding and decoding of codable record properties - [#393](https://github.com/groue/GRDB.swift/pull/393): Upgrade SQLCipher to 3.4.2, enable FTS5 on GRDBCipher and new pod GRDBPlus. +- [#384](https://github.com/groue/GRDB.swift/pull/384): Improve database value decoding diagnostics ### API diff +Cursors of optimized values (Strint, Int, Date, etc.) have been renamed: use FastDatabaseValueCursor and FastNullableDatabaseValueCursor instead of the deprecated ColumnCursor and NullableColumnCursor. + ```diff +final class FastDatabaseValueCursor : Cursor { } +@available(*, deprecated, renamed: "FastDatabaseValueCursor") @@ -22,7 +24,8 @@ Release Notes ### Documentation Diff -- [Enabling FTS5 Support](README.md#enabling-fts5-support): Procedure for enabling FTS5 support in GRDB +- [Enabling FTS5 Support](README.md#enabling-fts5-support): Procedure for enabling FTS5 support in GRDB. +- [Codable Records](README.md#codable-records): Documentation for JSON encoding of codable properties in records. ## 3.2.0 From 7ea15dcce2c4d2e7b01d930e0095dee412e3f353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Wed, 15 Aug 2018 19:44:47 +0200 Subject: [PATCH 37/37] Welcome @valexa and @gusrota to the thank you notice --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34f76ef4fd..9143af9a24 100644 --- a/README.md +++ b/README.md @@ -7424,7 +7424,7 @@ Sample Code **Thanks** - [Pierlis](http://pierlis.com), where we write great software. -- [Vladimir Babin](https://github.com/Chiliec), [Marcel Ball](https://github.com/Marus), [@bellebethcooper](https://github.com/bellebethcooper), [Darren Clark](https://github.com/darrenclark), [Pascal Edmond](https://github.com/pakko972), [Andrey Fidrya](https://github.com/zmeyc), [Cristian Filipov](https://github.com/cfilipov), [Matt Greenfield](https://github.com/sobri909), [David Hart](https://github.com/hartbit), [@kluufger](https://github.com/kluufger), [Brad Lindsay](https://github.com/bfad), [@peter-ss](https://github.com/peter-ss), [Florent Pillet](http://github.com/fpillet), [@pocketpixels](https://github.com/pocketpixels), [Pierre-Loïc Raynaud](https://github.com/pierlo), [Stefano Rodriguez](https://github.com/sroddy), [Steven Schveighoffer](https://github.com/schveiguy), [@swiftlyfalling](https://github.com/swiftlyfalling), and [Kevin Wooten](https://github.com/kdubb) for their contributions, help, and feedback on GRDB. +- [Vlad Alexa](https://github.com/valexa), [Vladimir Babin](https://github.com/Chiliec), [Marcel Ball](https://github.com/Marus), [@bellebethcooper](https://github.com/bellebethcooper), [Darren Clark](https://github.com/darrenclark), [Pascal Edmond](https://github.com/pakko972), [Andrey Fidrya](https://github.com/zmeyc), [Cristian Filipov](https://github.com/cfilipov), [Matt Greenfield](https://github.com/sobri909), [@gusrota](https://github.com/gusrota), [David Hart](https://github.com/hartbit), [@kluufger](https://github.com/kluufger), [Brad Lindsay](https://github.com/bfad), [@peter-ss](https://github.com/peter-ss), [Florent Pillet](http://github.com/fpillet), [@pocketpixels](https://github.com/pocketpixels), [Pierre-Loïc Raynaud](https://github.com/pierlo), [Stefano Rodriguez](https://github.com/sroddy), [Steven Schveighoffer](https://github.com/schveiguy), [@swiftlyfalling](https://github.com/swiftlyfalling), and [Kevin Wooten](https://github.com/kdubb) for their contributions, help, and feedback on GRDB. - [@aymerick](https://github.com/aymerick) and [Mathieu "Kali" Poumeyrol](https://github.com/kali) because SQL. - [ccgus/fmdb](https://github.com/ccgus/fmdb) for its excellency.