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 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/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 2874c85586..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(), @@ -131,6 +138,14 @@ 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 row = Row(sqliteStatement: sqliteStatement) + fatalConversionError( + to: T.self, + from: DatabaseValue(sqliteStatement: sqliteStatement, index: index), + conversionContext: ValueConversionContext(row).atColumn(Int(index))) +} + // MARK: - DatabaseValueConvertible /// Lossless conversions from database values and rows diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 046e7883ac..7dd33e89d8 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. /// @@ -1214,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 { @@ -1291,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 + } } @@ -1326,6 +1339,10 @@ private struct StatementCopyRowImpl : RowImpl { let lowercaseName = name.lowercased() return columnNames.index { $0.lowercased() == lowercaseName } } + + func copiedRow(_ row: Row) -> Row { + return row + } } @@ -1407,6 +1424,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.. Int? { return nil } + + func copiedRow(_ row: Row) -> Row { + return row + } } diff --git a/GRDB/Core/Support/Foundation/Data.swift b/GRDB/Core/Support/Foundation/Data.swift index c88608bf6a..80a49dfcdb 100644 --- a/GRDB/Core/Support/Foundation/Data.swift +++ b/GRDB/Core/Support/Foundation/Data.swift @@ -24,9 +24,15 @@ extension Data : DatabaseValueConvertible, StatementColumnConvertible { /// 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 { + 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 } - return data } } 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 cabe0a5c88..976ae455b5 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) } } @@ -456,6 +456,10 @@ 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): + // 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: diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index c58ccec4b7..98530e4148 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -1,12 +1,13 @@ +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. @@ -81,7 +82,18 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } else if row.impl.hasNull(atUncheckedIndex: index) { return nil } else { - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + do { + // 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, columnIndex: index, codingPath: codingPath + [key])) + } catch is JSONRequiredError { + guard let data = row.dataNoCopy(atIndex: index) else { + fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) + } + return try makeJSONDecoder().decode(type.self, from: data) + } } } @@ -120,7 +132,18 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } else if let type = T.self as? DatabaseValueConvertible.Type { return type.decode(from: row, atUncheckedIndex: index) as! T } else { - return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) + do { + // 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, columnIndex: index, codingPath: codingPath + [key])) + } catch is JSONRequiredError { + guard let data = row.dataNoCopy(atIndex: index) else { + fatalConversionError(to: T.self, from: row[index], conversionContext: ValueConversionContext(row).atColumn(index)) + } + return try makeJSONDecoder().decode(type.self, from: data) + } } } @@ -189,15 +212,15 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer } private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { - let row: Row + var row: Row + var columnIndex: Int var codingPath: [CodingKey] - let column: CodingKey - + /// 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. @@ -206,20 +229,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. /// @@ -228,49 +251,66 @@ 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. - 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: RowDecoder(row: row, codingPath: [column])) + return try T(from: RowSingleValueDecoder(row: row, columnIndex: columnIndex, codingPath: codingPath)) } } } private struct RowDecoder: Decoder { - let row: Row + var row: Row + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey : Any] { return [:] } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + return KeyedDecodingContainer(RowKeyedDecodingContainer(decoder: self)) + } - init(row: Row, codingPath: [CodingKey]) { - self.row = row - self.codingPath = codingPath + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + throw JSONRequiredError() } - // Decoder - let codingPath: [CodingKey] + func singleValueContainer() throws -> SingleValueDecodingContainer { + throw JSONRequiredError() + } +} + +private struct RowSingleValueDecoder: Decoder { + var row: Row + var columnIndex: Int + var codingPath: [CodingKey] var userInfo: [CodingUserInfoKey : Any] { return [:] } func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { - return KeyedDecodingContainer(RowKeyedDecodingContainer(decoder: self)) + throw JSONRequiredError() } func unkeyedContainer() throws -> UnkeyedDecodingContainer { - throw DecodingError.typeMismatch( - UnkeyedDecodingContainer.self, - DecodingError.Context(codingPath: codingPath, debugDescription: "unkeyed decoding is not supported")) + throw JSONRequiredError() } 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) + return RowSingleValueDecodingContainer(row: row, columnIndex: columnIndex, codingPath: codingPath) } } +/// 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 135e8a149a..ac26dbdf01 100644 --- a/GRDB/Record/PersistableRecord+Encodable.swift +++ b/GRDB/Record/PersistableRecord+Encodable.swift @@ -1,7 +1,9 @@ +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 } @@ -35,12 +37,29 @@ 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 { - try value.encode(to: PersistableRecordEncoder(codingPath: [key], encode: encode)) + 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 JSONRequiredError { + // Encode to JSON + 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 + // 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)! // force unwrap because json data is guaranteed to convert to String + encode(jsonString, key.stringValue) + } } } @@ -98,10 +117,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. /// @@ -140,58 +163,208 @@ 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 { + // 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 JSONRequiredError { + // Encode to JSON + 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 + // 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)! // force unwrap because 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: 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 { + // 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) + } - let encode: (_ value: DatabaseValueConvertible?, _ key: String) -> Void + func singleValueContainer() -> SingleValueEncodingContainer { + return DatabaseValueEncodingContainer(key: key, encode: encode) + } +} + +private struct PersistableRecordEncoder: Encoder { + var codingPath: [CodingKey] = [] + var userInfo: [CodingUserInfoKey: Any] = [:] + var encode: DatabaseValuePersistenceEncoder - init(codingPath: [CodingKey], encode: @escaping (_ value: DatabaseValueConvertible?, _ key: String) -> Void) { - self.codingPath = codingPath + 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 { - fatalError("unkeyed encoding is not supported") - } 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 { 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") + } +} + +private struct JSONRequiredEncoder: Encoder { + var codingPath: [CodingKey] + var userInfo: [CodingUserInfoKey: Any] = [:] + + init(codingPath: [CodingKey]) { + self.codingPath = codingPath + } + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + return KeyedEncodingContainer(JSONRequiredKeyedContainer(codingPath: codingPath)) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + return JSONRequiredUnkeyedContainer(codingPath: codingPath) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + return JSONRequiredSingleValueContainer() + } +} + +private struct JSONRequiredKeyedContainer: KeyedEncodingContainerProtocol { + var codingPath: [CodingKey] + + 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(JSONRequiredKeyedContainer(codingPath: codingPath + [key])) + } + + func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer { + return JSONRequiredUnkeyedContainer(codingPath: codingPath) + } + + func superEncoder() -> Encoder { + return JSONRequiredEncoder(codingPath: codingPath) + } + + func superEncoder(forKey key: KeyType) -> Encoder { + return JSONRequiredEncoder(codingPath: codingPath) + } +} + +private struct JSONRequiredUnkeyedContainer: UnkeyedEncodingContainer { + var codingPath: [CodingKey] + var count: Int { return 0 } + + 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(JSONRequiredKeyedContainer(codingPath: codingPath)) + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + return self + } + + func superEncoder() -> Encoder { + return JSONRequiredEncoder(codingPath: codingPath) + } +} + +private struct JSONRequiredSingleValueContainer: SingleValueEncodingContainer { + var codingPath: [CodingKey] { return [] } + + 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 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 { @@ -200,9 +373,9 @@ extension MutablePersistableRecord where Self: Encodable { // 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) } } 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/README.md b/README.md index 4241e57132..9143af9a24 100644 --- a/README.md +++ b/README.md @@ -2572,10 +2572,10 @@ 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 plain Codable struct or class... +// Declare a Codable struct or class... struct Player: Codable { - let name: String - let score: Int + var name: String + var score: Int } // Adopt Record protocols... @@ -2588,70 +2588,41 @@ 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: +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 -// Can't take profit from Codable code generation: -struct Place: FetchableRecord, PersistableRecord, Codable { - var title: String - var coordinate: CLLocationCoordinate2D // <- Not a simple value! +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 @@ -7453,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. diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index b0fdb01f7c..d5463f0efe 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -321,7 +321,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } } - func testStatementColumnConvertible() throws { + func testStatementColumnConvertible1() throws { // Those tests are tightly coupled to GRDB decoding code. // Each test comes with one or several commented crashing code snippets that trigger it. let dbQueue = try makeDatabaseQueue() @@ -358,6 +358,54 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } } + func testStatementColumnConvertible2() throws { + // Those tests are tightly coupled to GRDB decoding code. + // Each test comes with one or several commented crashing code snippets that trigger it. + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS foo") + statement.arguments = [1000] + let row = try Row.fetchOne(statement)! + + // _ = try Row.fetchCursor(statement).map { $0["missing"] as Int8 }.next() + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Int8.self, + from: nil, + conversionContext: ValueConversionContext(row).atColumn("missing")), + "could not read Int8 from missing column `missing` (row: [foo:1000], sql: `SELECT ? AS foo`, arguments: [1000])") + } + + // _ = try Int8.fetchAll(statement) + try statement.makeCursor().forEach { + let sqliteStatement = statement.sqliteStatement + XCTAssertEqual( + conversionErrorMessage( + to: Int8.self, + from: DatabaseValue(sqliteStatement: sqliteStatement, index: 0), + conversionContext: ValueConversionContext(Row(sqliteStatement: sqliteStatement)).atColumn(0)), + "could not convert database value 1000 to Int8 (column: `foo`, column index: 0, row: [foo:1000], sql: `SELECT ? AS foo`)") + } + + // _ = row["name"] as Int8 + XCTAssertEqual( + conversionErrorMessage( + to: Int8.self, + from: row["foo"], + conversionContext: ValueConversionContext(row).atColumn("foo")), + "could not convert database value 1000 to Int8 (column: `foo`, column index: 0, row: [foo:1000])") + + // _ = row[0] as Int8 + XCTAssertEqual( + conversionErrorMessage( + to: Int8.self, + from: row[0], + conversionContext: ValueConversionContext(row).atColumn(0)), + "could not convert database value 1000 to Int8 (column: `foo`, column index: 0, row: [foo:1000])") + } + } + func testDecodableDatabaseValueConvertible() throws { enum Value: String, DatabaseValueConvertible, Decodable { case valid diff --git a/Tests/GRDBTests/DatabaseValueConversionTests.swift b/Tests/GRDBTests/DatabaseValueConversionTests.swift index d25b23bca6..fc0ae3a832 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,98 @@ 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 } } } +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 { + 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) + if let s = sqliteConversion as? String { + print(s.utf8.map { $0 }) + } + 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 dbValue = try DatabaseValue.fetchOne(db, sql)! + let dbValueConversion = T.fromDatabaseValue(dbValue) + XCTAssert( + dbValueConversion == expectedDatabaseValueConversion, + "unexpected SQLite conversion: \(stringRepresentation(dbValueConversion)) instead of \(stringRepresentation(expectedDatabaseValueConversion))", + file: file, line: line) + } + } + + 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 { @@ -61,22 +136,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") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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 +172,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") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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 +188,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") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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,58 +237,84 @@ 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") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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 + } + + // emojiString is turned to Text + + try dbQueue.inTransaction { db in + 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) + 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 } - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + // emojiData is turned to Blob 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éı👨👨🏿🇫🇷🇨🇮'") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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)]) - 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) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'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) + 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: invalidString, expectedDatabaseValueConversion: nil) + 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 { @@ -215,7 +337,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 +366,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 +402,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 +418,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 +434,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 +450,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 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) + 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 +482,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,55 +498,84 @@ 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 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) 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éı👨👨🏿🇫🇷🇨🇮'"]) - 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éı👨👨🏿🇫🇷🇨🇮'") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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) + 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 + // emojiData is turned to Blob 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) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + 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 } + + // nonUTF8Data is turned to Blob + + try dbQueue.inTransaction { db in + 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) + 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: invalidString, expectedDatabaseValueConversion: nil) + 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 { @@ -419,22 +587,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 +623,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 +639,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 +655,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,39 +687,84 @@ 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") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - - 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)]) - 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) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".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 + } + + // emojiString is turned to Text + + try dbQueue.inTransaction { db in + 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) + 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 + } + + // emojiData is turned to Blob + + try dbQueue.inTransaction { db in + 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) + 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 + } + + // 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: invalidString, expectedDatabaseValueConversion: nil) + 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 { @@ -557,22 +790,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 +826,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 +842,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 +858,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 +874,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 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) + 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 +906,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,54 +922,83 @@ 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 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) + return .rollback + } + + // emojiString is turned to Text + + try dbQueue.inTransaction { db in + 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) + 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 + } + + // 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 } - // "'fooéı👨👨🏿🇫🇷🇨🇮'" is turned to Text + // nonUTF8Data is turned to Blob 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éı👨👨🏿🇫🇷🇨🇮'") - XCTAssertTrue(Data.fromDatabaseValue(dbValue) == nil) - + 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) + 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: invalidString, expectedDatabaseValueConversion: nil) + try assertDecoding(db, sql, Data.self, expectedSQLiteConversion: nonUTF8Data, expectedDatabaseValueConversion: nonUTF8Data) return .rollback } - // Blob is turned to Blob + #if !SWIFT_PACKAGE + // jpegData is turned to Blob 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) - XCTAssertTrue(String.fromDatabaseValue(dbValue) == nil) - XCTAssertEqual(Data.fromDatabaseValue(dbValue), "'fooéı👨👨🏿🇫🇷🇨🇮'".data(using: .utf8)) - + 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 } } diff --git a/Tests/GRDBTests/FetchableRecordDecodableTests.swift b/Tests/GRDBTests/FetchableRecordDecodableTests.swift index 03ab933121..c5bae1bbf6 100644 --- a/Tests/GRDBTests/FetchableRecordDecodableTests.swift +++ b/Tests/GRDBTests/FetchableRecordDecodableTests.swift @@ -301,3 +301,453 @@ extension FetchableRecordDecodableTests { XCTAssertEqual(value.uuid, uuid) } } + +// MARK: - Custom nested Decodable types - nested saved as JSON + +extension FetchableRecordDecodableTests { + func testOptionalNestedStruct() throws { + struct NestedStruct : Codable { + let firstName: String? + let lastName: String? + } + + 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 nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: nested) + 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 : Codable { + let firstName: String? + let lastName: String? + } + + 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 : Codable { + let firstName: String? + let lastName: String? + } + + 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 nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: [nested, nested]) + 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: Codable { + let firstName: String? + let lastName: String? + } + + 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: Codable { + let firstName: String? + let lastName: String? + } + + 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 nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: nested) + 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 : Codable { + let firstName: String? + let lastName: String? + } + + 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 nested = NestedStruct(firstName: "Bob", lastName: "Dylan") + let value = StructWithNestedType(nested: [nested, nested]) + 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 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] + } + + // 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], timeline: timeline) + try value.insert(db) + + let parentModel = try Player.fetchAll(db) + + 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(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) + } + + } + + // 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) + } + } + } + + 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 + 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/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") } } diff --git a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift index 70f238d7a9..fd340eda9f 100644 --- a/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift +++ b/Tests/GRDBTests/MutablePersistableRecordEncodableTests.swift @@ -415,4 +415,62 @@ extension MutablePersistableRecordEncodableTests { XCTAssertEqual(fetchedUUID, value.uuid) } } + + 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 + 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]") + } + } } diff --git a/Tests/GRDBTests/PersistableRecordTests.swift b/Tests/GRDBTests/PersistableRecordTests.swift index 3d101212f9..3314612aaf 100644 --- a/Tests/GRDBTests/PersistableRecordTests.swift +++ b/Tests/GRDBTests/PersistableRecordTests.swift @@ -761,3 +761,285 @@ class PersistableRecordTests: GRDBTestCase { } } } + +// MARK: - Custom nested Codable types - nested saved as JSON + +extension PersistableRecordTests { + + func testOptionalNestedStruct() 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.firstName) + XCTAssertEqual(nested.lastName, decoded.lastName) + } catch { + XCTFail(error.localizedDescription) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } + + func testOptionalNestedStructNil() throws { + struct NestedStruct : Encodable { + let firstName: String? + let lastName: String? + } + + 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 : 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, 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(decoded.count, 2) + 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) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } + + func testOptionalNestedArrayStructNil() throws { + struct NestedStruct : Encodable { + let firstName: String? + let lastName: String? + } + + 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 : 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.firstName) + XCTAssertEqual(nested.lastName, decoded.lastName) + } catch { + XCTFail(error.localizedDescription) + } + } else { + XCTFail("Failed to convert " + string) + } + } + } + + 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) + } + } + +} 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 - } - } -}