From 1c811a61cae77a14babb721d5b4b33e0526f8939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 12 Jul 2018 22:01:24 +0200 Subject: [PATCH 01/31] improve value conversion diagnostics --- GRDB/Core/DatabaseValue.swift | 104 ++++++++++++------ GRDB/Core/DatabaseValueConvertible.swift | 4 +- GRDB/Core/Row.swift | 103 +++++++++-------- GRDB/Core/RowAdapter.swift | 23 ++-- GRDB/Core/StatementColumnConvertible.swift | 33 +++--- .../DatabaseValueConvertible+Decodable.swift | 30 ++--- GRDB/Record/FetchableRecord+Decodable.swift | 6 +- 7 files changed, 179 insertions(+), 124 deletions(-) diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 3b804a745f..fd3697249e 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -206,6 +206,75 @@ extension DatabaseValue { // MARK: - Lossless conversions +struct ValueConversionDebuggingInfo { + var row: Row? + var sql: String? + var arguments: StatementArguments? + var columnIndex: Int? + var columnName: String? + + init(row: Row? = nil, sql: String? = nil, arguments: StatementArguments? = nil, columnIndex: Int? = nil, columnName: String? = nil) { + self.row = row + self.sql = sql + self.arguments = arguments + self.columnIndex = columnIndex + self.columnName = columnName + } +} + +struct ValueConversionError: Error, CustomStringConvertible { + var dbValue: DatabaseValue + var debugInfo: ValueConversionDebuggingInfo + + var description: String { + var error = "could not convert database value \(dbValue) to \(T.self)" + if let columnName = debugInfo.columnName { + error += " at column `\(columnName)`" + } + if let columnIndex = debugInfo.columnIndex { + error += " at index `\(columnIndex)`" + } + if let row = debugInfo.row { + error += " in row `\(row)`" + } + let sql = debugInfo.sql ?? debugInfo.row?.statementRef?.takeUnretainedValue().sql + if let sql = sql { + error += " from statement `\(sql)`" + } + if let arguments = debugInfo.arguments, !arguments.isEmpty { + error += " arguments \(arguments)" + } + return error + } +} + +extension DatabaseValueConvertible { + /// Performs lossless conversion from a database value. + /// + /// - throws: ValueConversionError + static func convert(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { + if let value = fromDatabaseValue(dbValue) { + return value + } else { + throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) + } + } + + /// Performs lossless conversion from a database value. + /// + /// - throws: ValueConversionError + static func convertOptional(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self? { + // Use fromDatabaseValue before checking for null: this allows DatabaseValue to convert NULL to .null. + if let value = fromDatabaseValue(dbValue) { + return value + } else if dbValue.isNull { + return nil + } else { + throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) + } + } +} + extension DatabaseValue { /// Converts the database value to the type T. /// @@ -225,19 +294,9 @@ extension DatabaseValue { /// conversion error /// - arguments: Optional statement arguments that enhances the eventual /// conversion error + @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible { - if let value = T.fromDatabaseValue(self) { - return value - } - // Failed conversion: this is data loss, a programmer error. - var error = "could not convert database value \(self) to \(T.self)" - if let sql = sql { - error += " with statement `\(sql)`" - } - if let arguments = arguments, !arguments.isEmpty { - error += " arguments \(arguments)" - } - fatalError(error) + return try! T.convert(from: self, debugInfo: ValueConversionDebuggingInfo()) } /// Converts the database value to the type Optional. @@ -259,26 +318,9 @@ extension DatabaseValue { /// conversion error /// - arguments: Optional statement arguments that enhances the eventual /// conversion error + @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible { - // Use fromDatabaseValue first: this allows DatabaseValue to convert NULL to .null. - if let value = T.fromDatabaseValue(self) { - return value - } - if isNull { - // Failed conversion from null: ok - return nil - } else { - // Failed conversion from a non-null database value: this is data - // loss, a programmer error. - var error = "could not convert database value \(self) to \(T.self)" - if let sql = sql { - error += " with statement `\(sql)`" - } - if let arguments = arguments, !arguments.isEmpty { - error += " arguments \(arguments)" - } - fatalError(error) - } + return try! T.convertOptional(from: self, debugInfo: ValueConversionDebuggingInfo()) } } diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index 2a72042eb7..ed064b4c07 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -70,7 +70,7 @@ public final class DatabaseValueCursor : Cursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return dbValue.losslessConvert() as Value + return try! Value.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo(sql: statement.sql, arguments: statement.arguments, columnIndex: Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) @@ -110,7 +110,7 @@ public final class NullableDatabaseValueCursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return dbValue.losslessConvert() as Value? + return try! Value.convertOptional(from: dbValue, debugInfo: ValueConversionDebuggingInfo(sql: statement.sql, arguments: statement.arguments, columnIndex: Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 804054651c..dc6172f739 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -187,7 +187,7 @@ extension Row { /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return impl.value(atUncheckedIndex: index) + return try! impl.optionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -205,9 +205,9 @@ extension Row { public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") if let sqliteStatement = sqliteStatement { // fast path - return Value.losslessConvert(sqliteStatement: sqliteStatement, index: Int32(index)) + return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) } - return impl.fastValue(atUncheckedIndex: index) + return try! impl.fastOptionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -219,7 +219,7 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return impl.value(atUncheckedIndex: index) + return try! impl.value(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -236,9 +236,9 @@ extension Row { public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") if let sqliteStatement = sqliteStatement { // fast path - return Value.losslessConvert(sqliteStatement: sqliteStatement, index: Int32(index)) + return try! Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } - return impl.fastValue(atUncheckedIndex: index) + return try! impl.fastValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -274,7 +274,7 @@ extension Row { guard let index = impl.index(ofColumn: columnName) else { return nil } - return impl.value(atUncheckedIndex: index) + return try! impl.optionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -294,9 +294,9 @@ extension Row { return nil } if let sqliteStatement = sqliteStatement { // fast path - return Value.losslessConvert(sqliteStatement: sqliteStatement, index: Int32(index)) + return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) } - return impl.fastValue(atUncheckedIndex: index) + return try! impl.fastOptionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -313,7 +313,7 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return impl.value(atUncheckedIndex: index) + return try! impl.value(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -335,9 +335,9 @@ extension Row { fatalError("no such column: \(columnName)") } if let sqliteStatement = sqliteStatement { // fast path - return Value.losslessConvert(sqliteStatement: sqliteStatement, index: Int32(index)) + return try! Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } - return impl.fastValue(atUncheckedIndex: index) + return try! impl.fastValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -421,7 +421,7 @@ extension Row { /// than the row's lifetime. public func dataNoCopy(atIndex index: Int) -> Data? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return impl.dataNoCopy(atUncheckedIndex: index) + return try! impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the optional Data at given column. @@ -439,7 +439,7 @@ extension Row { guard let index = impl.index(ofColumn: columnName) else { return nil } - return impl.dataNoCopy(atUncheckedIndex: index) + return try! impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the optional `NSData` at given column. @@ -1178,12 +1178,12 @@ protocol RowImpl { var count: Int { get } var isFetched: Bool { get } var scopes: Row.ScopesView { get } - func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue - func fastValue(atUncheckedIndex index: Int) -> Value - func fastValue(atUncheckedIndex index: Int) -> Value? - func hasNull(atUncheckedIndex index:Int) -> Bool - func dataNoCopy(atUncheckedIndex index:Int) -> Data? func columnName(atUncheckedIndex index: Int) -> String + func hasNull(atUncheckedIndex index:Int) -> Bool + func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue + func fastValue(atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + func fastOptionalValue(atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? // This method MUST be case-insensitive, and returns the index of the // leftmost column that matches *name*. @@ -1216,20 +1216,38 @@ extension RowImpl { return databaseValue(atUncheckedIndex: index).isNull } - func value(atUncheckedIndex index: Int) -> Value { - return databaseValue(atUncheckedIndex: index).losslessConvert() + func value( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + return try Value.convert(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } - func value(atUncheckedIndex index: Int) -> Value? { - return databaseValue(atUncheckedIndex: index).losslessConvert() + func optionalValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + return try Value.convertOptional(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } - func fastValue(atUncheckedIndex index: Int) -> Value { - return databaseValue(atUncheckedIndex: index).losslessConvert() + func fastValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + // default fast implementation is slow + return try value(atUncheckedIndex: index, debugInfo: debugInfo) } - func fastValue(atUncheckedIndex index: Int) -> Value? { - return databaseValue(atUncheckedIndex: index).losslessConvert() + func fastOptionalValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + // default fast implementation is slow + return try optionalValue(atUncheckedIndex: index, debugInfo: debugInfo) + } + + func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { + return try Data.convertOptional(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } } @@ -1249,10 +1267,6 @@ private struct ArrayRowImpl : RowImpl { return false } - func dataNoCopy(atUncheckedIndex index:Int) -> Data? { - return databaseValue(atUncheckedIndex: index).losslessConvert() - } - func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { return columns[index].1 } @@ -1289,10 +1303,6 @@ private struct StatementCopyRowImpl : RowImpl { return true } - func dataNoCopy(atUncheckedIndex index:Int) -> Data? { - return databaseValue(atUncheckedIndex: index).losslessConvert() - } - func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { return dbValues[index] } @@ -1341,7 +1351,7 @@ private struct StatementRowImpl : RowImpl { return sqlite3_column_type(sqliteStatement, Int32(index)) == SQLITE_NULL } - func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { guard sqlite3_column_type(sqliteStatement, Int32(index)) != SQLITE_NULL else { return nil } @@ -1356,14 +1366,20 @@ private struct StatementRowImpl : RowImpl { return DatabaseValue(sqliteStatement: sqliteStatement, index: Int32(index)) } - func fastValue(atUncheckedIndex index: Int) -> Value { - return Value.losslessConvert(sqliteStatement: sqliteStatement, index: Int32(index)) + func fastValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + return try Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: debugInfo) } - func fastValue(atUncheckedIndex index: Int) -> Value? { - return Value.losslessConvert(sqliteStatement: sqliteStatement, index: Int32(index)) + func fastOptionalValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) } - + func columnName(atUncheckedIndex index: Int) -> String { return statementRef.takeUnretainedValue().columnNames[index] } @@ -1397,11 +1413,6 @@ private struct EmptyRowImpl : RowImpl { fatalError("row index out of range") } - func dataNoCopy(atUncheckedIndex index:Int) -> Data? { - // Programmer error - fatalError("row index out of range") - } - func columnName(atUncheckedIndex index: Int) -> String { // Programmer error fatalError("row index out of range") diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index f322bf4d28..258ee9d54a 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -501,19 +501,28 @@ struct AdaptedRowImpl : RowImpl { return base.impl.databaseValue(atUncheckedIndex: mappedIndex) } - func fastValue(atUncheckedIndex index: Int) -> Value { + func fastValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.fastValue(atUncheckedIndex: mappedIndex) + return try base.impl.fastValue(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) } - func fastValue(atUncheckedIndex index: Int) -> Value? { + func fastOptionalValue( + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.fastValue(atUncheckedIndex: mappedIndex) + return try base.impl.fastOptionalValue(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) } - - func dataNoCopy(atUncheckedIndex index:Int) -> Data? { + + func dataNoCopy( + atUncheckedIndex index:Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? + { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.dataNoCopy(atUncheckedIndex: mappedIndex) + return try base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) } func columnName(atUncheckedIndex index: Int) -> String { diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 499c39dd9a..b0e56f1709 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -50,21 +50,24 @@ public protocol StatementColumnConvertible { init(sqliteStatement: SQLiteStatement, index: Int32) } -extension StatementColumnConvertible { +extension DatabaseValueConvertible where Self: StatementColumnConvertible { + /// Performs lossless conversion from a statement value. + /// + /// - throws: ValueConversionError @inline(__always) - static func losslessConvert(sqliteStatement: SQLiteStatement, index: Int32) -> Self? { - guard sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL else { - return nil + static func convert(sqliteStatement: SQLiteStatement, index: Int32, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { + if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { + throw ValueConversionError(dbValue: .null, debugInfo: debugInfo()) } return self.init(sqliteStatement: sqliteStatement, index: index) } + /// Performs lossless conversion from a statement value. @inline(__always) - static func losslessConvert(sqliteStatement: SQLiteStatement, index: Int32) -> Self { - guard sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL else { - // Programmer error - fatalError("could not convert database value NULL to \(Self.self)") + static func convertOptional(sqliteStatement: SQLiteStatement, index: Int32) -> Self? { + if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { + return nil } return self.init(sqliteStatement: sqliteStatement, index: index) } @@ -102,14 +105,7 @@ public final class ColumnCursor Bool { return dbValue.losslessConvert() } - func decode(_ type: Int.Type) throws -> Int { return dbValue.losslessConvert() } - func decode(_ type: Int8.Type) throws -> Int8 { return dbValue.losslessConvert() } - func decode(_ type: Int16.Type) throws -> Int16 { return dbValue.losslessConvert() } - func decode(_ type: Int32.Type) throws -> Int32 { return dbValue.losslessConvert() } - func decode(_ type: Int64.Type) throws -> Int64 { return dbValue.losslessConvert() } - func decode(_ type: UInt.Type) throws -> UInt { return dbValue.losslessConvert() } - func decode(_ type: UInt8.Type) throws -> UInt8 { return dbValue.losslessConvert() } - func decode(_ type: UInt16.Type) throws -> UInt16 { return dbValue.losslessConvert() } - func decode(_ type: UInt32.Type) throws -> UInt32 { return dbValue.losslessConvert() } - func decode(_ type: UInt64.Type) throws -> UInt64 { return dbValue.losslessConvert() } - func decode(_ type: Float.Type) throws -> Float { return dbValue.losslessConvert() } - func decode(_ type: Double.Type) throws -> Double { return dbValue.losslessConvert() } - func decode(_ type: String.Type) throws -> String { return dbValue.losslessConvert() } + func decode(_ type: Bool.Type) throws -> Bool { return try! Bool.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int.Type) throws -> Int { return try! Int.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int8.Type) throws -> Int8 { return try! Int8.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int16.Type) throws -> Int16 { return try! Int16.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int32.Type) throws -> Int32 { return try! Int32.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int64.Type) throws -> Int64 { return try! Int64.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt.Type) throws -> UInt { return try! UInt.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt8.Type) throws -> UInt8 { return try! UInt8.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt16.Type) throws -> UInt16 { return try! UInt16.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt32.Type) throws -> UInt32 { return try! UInt32.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt64.Type) throws -> UInt64 { return try! UInt64.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Float.Type) throws -> Float { return try! Float.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Double.Type) throws -> Double { return try! Double.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: String.Type) throws -> String { return try! String.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } /// Decodes a single value of the given type. /// @@ -41,7 +41,7 @@ private struct DatabaseValueDecodingContainer: SingleValueDecodingContainer { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows custom database decoding, such as decoding Date from // String, for example. - return type.fromDatabaseValue(dbValue) as! T + return try! type.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) as! T } else { return try T(from: DatabaseValueDecoder(dbValue: dbValue, codingPath: codingPath)) } diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 44e510cfa2..301fda70a9 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -75,7 +75,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer 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.fromDatabaseValue(dbValue) as! T? + return try! type.convertOptional(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T? } else if dbValue.isNull { return nil } else { @@ -114,7 +114,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer 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.fromDatabaseValue(dbValue) as! T + return try! type.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T } else { return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } @@ -227,7 +227,7 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return type.fromDatabaseValue(row[column.stringValue]) as! T + return try! type.convert(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(row: row, columnName: column.stringValue)) as! T } else { return try T(from: RowDecoder(row: row, codingPath: [column])) } From a8f6ed6d619477227a51612950e8ce8158c3628e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 12 Jul 2018 23:20:55 +0200 Subject: [PATCH 02/31] improve value conversion diagnostics more --- GRDB.xcodeproj/project.pbxproj | 6 ++ GRDB/Core/DatabaseValue.swift | 31 ++++--- GRDB/Core/DatabaseValueConvertible.swift | 4 +- GRDB/Core/Row.swift | 89 +++++++++++++------ GRDB/Core/RowAdapter.swift | 10 ++- GRDB/Core/StatementColumnConvertible.swift | 2 +- .../DatabaseValueConvertibleErrorTests.swift | 80 +++++++++++++++++ 7 files changed, 173 insertions(+), 49 deletions(-) create mode 100644 Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 4ee666f455..2958b2dc44 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -148,6 +148,8 @@ 564F9C2D1F075DD200877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; 564F9C311F07611600877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; 564F9C341F07611900877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; + 564FCE5E20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */; }; + 564FCE5F20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */; }; 5653EAD620944B4F00F46237 /* AssociationChainRowScopesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */; }; 5653EAD720944B4F00F46237 /* AssociationChainRowScopesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */; }; 5653EAD820944B4F00F46237 /* AssociationBelongsToRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EAC720944B4C00F46237 /* AssociationBelongsToRowScopeTests.swift */; }; @@ -869,6 +871,7 @@ 564E73DE203D50B9000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseFunction.swift; sourceTree = ""; }; + 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleErrorTests.swift; sourceTree = ""; }; 5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationChainRowScopesTests.swift; sourceTree = ""; }; 5653EAC720944B4C00F46237 /* AssociationBelongsToRowScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationBelongsToRowScopeTests.swift; sourceTree = ""; }; 5653EAC820944B4D00F46237 /* AssociationRowScopeSearchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationRowScopeSearchTests.swift; sourceTree = ""; }; @@ -1584,6 +1587,7 @@ children = ( 5674A70A1F3087700095F066 /* DatabaseValueConvertibleDecodableTests.swift */, 5674A70B1F3087700095F066 /* DatabaseValueConvertibleEncodableTests.swift */, + 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */, 56A238B51B9CA2590082EB20 /* DatabaseTimestampTests.swift */, 56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */, 56A2381A1B9C74A90082EB20 /* DatabaseValueConvertibleSubclassTests.swift */, @@ -2603,6 +2607,7 @@ 567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, 5657AB621D108BA9006283EF /* FoundationNSURLTests.swift in Sources */, 5657AB6A1D108BA9006283EF /* FoundationURLTests.swift in Sources */, + 564FCE5F20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */, 5653EADF20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */, 56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */, 56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */, @@ -2765,6 +2770,7 @@ 56D496601D81304E008276D7 /* FoundationNSUUIDTests.swift in Sources */, 567F45A81F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, 56D4968D1D81316E008276D7 /* DatabaseFunctionTests.swift in Sources */, + 564FCE5E20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */, 5653EADE20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */, 56D496661D813086008276D7 /* QueryInterfaceRequestTests.swift in Sources */, 56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */, diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index fd3697249e..117e7bf2a3 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -207,16 +207,14 @@ extension DatabaseValue { // MARK: - Lossless conversions struct ValueConversionDebuggingInfo { + var statement: SelectStatement? var row: Row? - var sql: String? - var arguments: StatementArguments? var columnIndex: Int? var columnName: String? - init(row: Row? = nil, sql: String? = nil, arguments: StatementArguments? = nil, columnIndex: Int? = nil, columnName: String? = nil) { + init(statement: SelectStatement? = nil, row: Row? = nil, columnIndex: Int? = nil, columnName: String? = nil) { + self.statement = statement self.row = row - self.sql = sql - self.arguments = arguments self.columnIndex = columnIndex self.columnName = columnName } @@ -228,21 +226,26 @@ struct ValueConversionError: Error, CustomStringConvertible { var description: String { var error = "could not convert database value \(dbValue) to \(T.self)" + var extras: [String] = [] if let columnName = debugInfo.columnName { - error += " at column `\(columnName)`" + extras.append("column: `\(columnName)`") } if let columnIndex = debugInfo.columnIndex { - error += " at index `\(columnIndex)`" + extras.append("index: \(columnIndex)") } - if let row = debugInfo.row { - error += " in row `\(row)`" + let row = debugInfo.row ?? debugInfo.statement.map { Row(statement: $0) } + if let row = row { + extras.append("row: \(row)") } - let sql = debugInfo.sql ?? debugInfo.row?.statementRef?.takeUnretainedValue().sql - if let sql = sql { - error += " from statement `\(sql)`" + let statement = debugInfo.statement ?? debugInfo.row?.statement + if let statement = statement { + extras.append("statement: `\(statement.sql)`") + if statement.arguments.isEmpty == false { + extras.append("arguments: \(statement.arguments)") + } } - if let arguments = debugInfo.arguments, !arguments.isEmpty { - error += " arguments \(arguments)" + if extras.isEmpty == false { + error += " (" + extras.joined(separator: ", ") + ")" } return error } diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index ed064b4c07..1ad76b5263 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -70,7 +70,7 @@ public final class DatabaseValueCursor : Cursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return try! Value.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo(sql: statement.sql, arguments: statement.arguments, columnIndex: Int(columnIndex))) + return try! Value.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) @@ -110,7 +110,7 @@ public final class NullableDatabaseValueCursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return try! Value.convertOptional(from: dbValue, debugInfo: ValueConversionDebuggingInfo(sql: statement.sql, arguments: statement.arguments, columnIndex: Int(columnIndex))) + return try! Value.convertOptional(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index dc6172f739..01d977f32e 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -21,6 +21,9 @@ public final class Row : Equatable, Hashable, RandomAccessCollection, Expressibl /// The statementRef is released in deinit. let statementRef: Unmanaged? let sqliteStatement: SQLiteStatement? + var statement: SelectStatement? { + return statementRef?.takeUnretainedValue() + } /// The number of columns in the row. public let count: Int @@ -177,6 +180,44 @@ extension Row { return impl.databaseValue(atUncheckedIndex: index).storage.value } + func decodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + return try impl.decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + func decode( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + return try impl.decode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + func fastDecodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + if let sqliteStatement = sqliteStatement { + return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) + } + return try impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + func fastDecode( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + if let sqliteStatement = sqliteStatement { + return try Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + } + return try impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + /// Returns the value at given index, converted to the requested type. /// /// Indexes span from 0 for the leftmost column to (row.count - 1) for the @@ -187,7 +228,7 @@ extension Row { /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! impl.optionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return try! decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -204,10 +245,7 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - if let sqliteStatement = sqliteStatement { // fast path - return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) - } - return try! impl.fastOptionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return try! fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -219,7 +257,7 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! impl.value(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return try! decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -235,10 +273,7 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - if let sqliteStatement = sqliteStatement { // fast path - return try! Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) - } - return try! impl.fastValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return try! fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -274,7 +309,7 @@ extension Row { guard let index = impl.index(ofColumn: columnName) else { return nil } - return try! impl.optionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return try! decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -293,10 +328,7 @@ extension Row { guard let index = impl.index(ofColumn: columnName) else { return nil } - if let sqliteStatement = sqliteStatement { // fast path - return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) - } - return try! impl.fastOptionalValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return try! fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -313,7 +345,7 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return try! impl.value(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return try! decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -334,10 +366,7 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - if let sqliteStatement = sqliteStatement { // fast path - return try! Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) - } - return try! impl.fastValue(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return try! fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -1181,8 +1210,8 @@ protocol RowImpl { func columnName(atUncheckedIndex index: Int) -> String func hasNull(atUncheckedIndex index:Int) -> Bool func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue - func fastValue(atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - func fastOptionalValue(atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + func fastDecode(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + func fastDecodeIfPresent(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? // This method MUST be case-insensitive, and returns the index of the @@ -1216,34 +1245,38 @@ extension RowImpl { return databaseValue(atUncheckedIndex: index).isNull } - func value( + func decode( + _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value { return try Value.convert(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } - func optionalValue( + func decodeIfPresent( + _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? { return try Value.convertOptional(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } - func fastValue( + func fastDecode( + _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value { // default fast implementation is slow - return try value(atUncheckedIndex: index, debugInfo: debugInfo) + return try decode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) } - func fastOptionalValue( + func fastDecodeIfPresent( + _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? { // default fast implementation is slow - return try optionalValue(atUncheckedIndex: index, debugInfo: debugInfo) + return try decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) } func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 258ee9d54a..9351440cbc 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -501,20 +501,22 @@ struct AdaptedRowImpl : RowImpl { return base.impl.databaseValue(atUncheckedIndex: mappedIndex) } - func fastValue( + func fastDecode( + _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.fastValue(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) + return try base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) } - func fastOptionalValue( + func fastDecodeIfPresent( + _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.fastOptionalValue(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) + return try base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) } func dataNoCopy( diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index b0e56f1709..141da0764e 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -105,7 +105,7 @@ public final class ColumnCursor Date: Fri, 13 Jul 2018 09:09:24 +0200 Subject: [PATCH 03/31] improve value conversion diagnostics more --- GRDB/Core/DatabaseValue.swift | 53 ++++++--- GRDB/Core/Row.swift | 103 +++++++++--------- .../DatabaseValueConvertibleErrorTests.swift | 100 ++++++++++------- 3 files changed, 149 insertions(+), 107 deletions(-) diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 117e7bf2a3..28962ef3b2 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -207,16 +207,45 @@ extension DatabaseValue { // MARK: - Lossless conversions struct ValueConversionDebuggingInfo { - var statement: SelectStatement? - var row: Row? - var columnIndex: Int? - var columnName: String? + var _statement: SelectStatement? + var _row: Row? + var _columnIndex: Int? + var _columnName: String? + var statement: SelectStatement? { + return _statement ?? _row?.statement + } + + var row: Row? { + return _row ?? _statement.map { Row(statement: $0) } + } + + var columnIndex: Int? { + if let columnIndex = _columnIndex { + return columnIndex + } + if let columnName = _columnName, let row = row { + return row.index(ofColumn: columnName) + } + return nil + } + + var columnName: String? { + if let columnName = _columnName { + return columnName + } + if let columnIndex = _columnIndex, let row = row { + let rowIndex = row.index(row.startIndex, offsetBy: columnIndex) + return row[rowIndex].0 + } + return nil + } + init(statement: SelectStatement? = nil, row: Row? = nil, columnIndex: Int? = nil, columnName: String? = nil) { - self.statement = statement - self.row = row - self.columnIndex = columnIndex - self.columnName = columnName + _statement = statement + _row = row + _columnIndex = columnIndex + _columnName = columnName } } @@ -231,14 +260,12 @@ struct ValueConversionError: Error, CustomStringConvertible { extras.append("column: `\(columnName)`") } if let columnIndex = debugInfo.columnIndex { - extras.append("index: \(columnIndex)") + extras.append("column index: \(columnIndex)") } - let row = debugInfo.row ?? debugInfo.statement.map { Row(statement: $0) } - if let row = row { + if let row = debugInfo.row { extras.append("row: \(row)") } - let statement = debugInfo.statement ?? debugInfo.row?.statement - if let statement = statement { + if let statement = debugInfo.statement { extras.append("statement: `\(statement.sql)`") if statement.arguments.isEmpty == false { extras.append("arguments: \(statement.arguments)") diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 01d977f32e..cbfab9bfe5 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -132,6 +132,50 @@ extension Row { extension Row { + // MARK: - Extracting values: primitive + + func index(ofColumn name: String) -> Int? { + return impl.index(ofColumn: name) + } + + func decodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + return try impl.decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + func decode( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + return try impl.decode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + func fastDecodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + if let sqliteStatement = sqliteStatement { + return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) + } + return try impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + func fastDecode( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + if let sqliteStatement = sqliteStatement { + return try Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + } + return try impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + // MARK: - Extracting Values /// Returns true if and only if one column contains a non-null value, or if @@ -180,44 +224,6 @@ extension Row { return impl.databaseValue(atUncheckedIndex: index).storage.value } - func decodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? - { - return try impl.decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - func decode( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - { - return try impl.decode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - func fastDecodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? - { - if let sqliteStatement = sqliteStatement { - return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) - } - return try impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - func fastDecode( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - { - if let sqliteStatement = sqliteStatement { - return try Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: debugInfo) - } - return try impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - /// Returns the value at given index, converted to the requested type. /// /// Indexes span from 0 for the leftmost column to (row.count - 1) for the @@ -291,7 +297,7 @@ extension Row { // if row["foo"] != nil { ... } // // Without this method, the code above would not compile. - guard let index = impl.index(ofColumn: columnName) else { + guard let index = index(ofColumn: columnName) else { return nil } return impl.databaseValue(atUncheckedIndex: index).storage.value @@ -306,7 +312,7 @@ extension Row { /// nil. Otherwise the SQLite value is converted to the requested type /// `Value`. Should this conversion fail, a fatal error is raised. public subscript(_ columnName: String) -> Value? { - guard let index = impl.index(ofColumn: columnName) else { + guard let index = index(ofColumn: columnName) else { return nil } return try! decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) @@ -325,7 +331,7 @@ extension Row { /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions /// (see https://www.sqlite.org/datatype3.html). public subscript(_ columnName: String) -> Value? { - guard let index = impl.index(ofColumn: columnName) else { + guard let index = index(ofColumn: columnName) else { return nil } return try! fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) @@ -341,7 +347,7 @@ extension Row { /// This method crashes if the fetched SQLite value is NULL, or if the /// SQLite value can not be converted to `Value`. public subscript(_ columnName: String) -> Value { - guard let index = impl.index(ofColumn: columnName) else { + guard let index = index(ofColumn: columnName) else { // Programmer error fatalError("no such column: \(columnName)") } @@ -362,7 +368,7 @@ extension Row { /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions /// (see https://www.sqlite.org/datatype3.html). public subscript(_ columnName: String) -> Value { - guard let index = impl.index(ofColumn: columnName) else { + guard let index = index(ofColumn: columnName) else { // Programmer error fatalError("no such column: \(columnName)") } @@ -465,7 +471,7 @@ extension Row { /// The returned data does not owns its bytes: it must not be used longer /// than the row's lifetime. public func dataNoCopy(named columnName: String) -> Data? { - guard let index = impl.index(ofColumn: columnName) else { + guard let index = index(ofColumn: columnName) else { return nil } return try! impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) @@ -1214,8 +1220,7 @@ protocol RowImpl { func fastDecodeIfPresent(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? - // This method MUST be case-insensitive, and returns the index of the - // leftmost column that matches *name*. + /// Returns the index of the leftmost column that matches *name* (case-insensitive) func index(ofColumn name: String) -> Int? // row.impl is guaranteed to be self. @@ -1308,8 +1313,6 @@ private struct ArrayRowImpl : RowImpl { return columns[index].0 } - // This method MUST be case-insensitive, and returns the index of the - // leftmost column that matches *name*. func index(ofColumn name: String) -> Int? { let lowercaseName = name.lowercased() return columns.index { (column, _) in column.lowercased() == lowercaseName } @@ -1344,8 +1347,6 @@ private struct StatementCopyRowImpl : RowImpl { return columnNames[index] } - // This method MUST be case-insensitive, and returns the index of the - // leftmost column that matches *name*. func index(ofColumn name: String) -> Int? { let lowercaseName = name.lowercased() return columnNames.index { $0.lowercased() == lowercaseName } @@ -1417,8 +1418,6 @@ private struct StatementRowImpl : RowImpl { return statementRef.takeUnretainedValue().columnNames[index] } - // This method MUST be case-insensitive, and returns the index of the - // leftmost column that matches *name*. func index(ofColumn name: String) -> Int? { if let index = lowercaseColumnIndexes[name] { return index diff --git a/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift index 4d63495899..28dd7226aa 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift @@ -24,57 +24,73 @@ class DatabaseValueConvertibleErrorTests: GRDBTestCase { } struct Record3: Codable, FetchableRecord { - var name: Value1 + var team: Value1 + } + + struct Record4: FetchableRecord { + var team: Value1 + + init(row: Row) { + team = row["team"] + } } enum Value1: String, DatabaseValueConvertible, Codable { - case baz + case valid } func testError() throws { // TODO: find a way to turn those into real tests let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in -// let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") -// statement.arguments = ["foo"] -// let row = try Row.fetchOne(statement)! -// -// // could not convert database value NULL to String (column: `name`, index: 0, row: [name:NULL team:"foo"]) -// _ = Record1(row: row) -// -// // could not convert database value NULL to String (column: `name`, index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) -// _ = try Record1.fetchOne(statement) -// -// // could not convert database value NULL to String (column: `name`, index: 0, row: [name:NULL team:"foo"]) -// _ = Record2(row: row) -// -// // could not convert database value NULL to String (column: `name`, index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) -// _ = try Record2.fetchOne(statement) -// -// // could not convert database value NULL to Value1 (column: `name`, row: [name:NULL team:"foo"]) -// _ = Record3(row: row) -// - // TODO: we miss column index below -// // could not convert database value NULL to Value1 (column: `name`, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) -// _ = try Record3.fetchOne(statement) -// -// // could not convert database value NULL to String (index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) -// _ = try String.fetchAll(statement) -// -// // could not convert database value NULL to String (column: `name`, index: 0, row: [name:NULL team:"foo"]) -// _ = row["name"] as String -// -// // could not convert database value NULL to String (index: 0, row: [name:NULL team:"foo"]) -// _ = row[0] as String -// -// // could not convert database value NULL to Value1 (index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) -// _ = try Value1.fetchAll(statement) -// -// // could not convert database value NULL to Value1 (column: `name`, index: 0, row: [name:NULL team:"foo"]) -// _ = row["name"] as Value1 -// -// // could not convert database value NULL to Value1 (index: 0, row: [name:NULL team:"foo"]) -// _ = row[0] as Value1 + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! + + // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) + _ = Record1(row: row) + + // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try Record1.fetchOne(statement) + + // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) + _ = Record2(row: row) + + // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try Record2.fetchOne(statement) + + // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) + _ = Record3(row: row) + + // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try Record3.fetchOne(statement) + + // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) + _ = Record4(row: row) + + // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try Record4.fetchOne(statement) + + // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try String.fetchAll(statement) + + // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) + _ = row["name"] as String + + // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"]) + _ = row[0] as String + + // could not convert database value NULL to Value1 (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try Value1.fetchAll(statement) + + // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) + _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) + + // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"foo"]) + _ = row["name"] as Value1 + + // could not convert database value NULL to Value1 (index: 0, row: [name:NULL team:"foo"]) + _ = row[0] as Value1 } } } From f8de01779c22cc467b61eb1c7829ab8996d2ac0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 13 Jul 2018 14:06:46 +0200 Subject: [PATCH 04/31] Cleanup --- GRDB.xcodeproj/project.pbxproj | 10 +- GRDB/Core/DatabaseValue.swift | 103 +--------- GRDB/Core/DatabaseValueConversion.swift | 180 ++++++++++++++++++ GRDB/Core/DatabaseValueConvertible.swift | 4 +- GRDB/Core/Row.swift | 94 ++------- GRDB/Core/StatementColumnConvertible.swift | 28 +-- .../DatabaseValueConvertible+Decodable.swift | 30 +-- GRDB/Record/FetchableRecord+Decodable.swift | 6 +- GRDBCipher.xcodeproj/project.pbxproj | 6 + GRDBCustom.xcodeproj/project.pbxproj | 6 + .../DatabaseValueConvertibleErrorTests.swift | 96 +++++----- 11 files changed, 293 insertions(+), 270 deletions(-) create mode 100644 GRDB/Core/DatabaseValueConversion.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 2958b2dc44..c4a6f4065a 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -140,6 +140,9 @@ 56439B581F4CA5AE0066043F /* PerformanceTests.sqlite in Resources */ = {isa = PBXBuildFile; fileRef = 56BB86111BA9886D001F9168 /* PerformanceTests.sqlite */; }; 564448831EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; 564448871EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; + 5644DE6D20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */; }; + 5644DE6E20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */; }; + 5644DE6F20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */; }; 564A50C81BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */; }; 564E73DF203D50B9000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73DE203D50B9000C443C /* JoinSupportTests.swift */; }; 564E73E0203D50B9000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73DE203D50B9000C443C /* JoinSupportTests.swift */; }; @@ -867,6 +870,7 @@ 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; 56439B501F4CA1DC0066043F /* GRDBOSXPerformanceComparisonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GRDBOSXPerformanceComparisonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = ""; }; + 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; }; 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; }; 564E73DE203D50B9000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; @@ -1659,6 +1663,7 @@ 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */, 566A84192041146100E50BFD /* DatabaseSnapshot.swift */, 56A238751B9C75030082EB20 /* DatabaseValue.swift */, + 5644DE6C20F8C32E001FFDDE /* DatabaseValueConversion.swift */, 560D923E1C672C3E00F4F92B /* DatabaseValueConvertible.swift */, 563363C31C942C37000BE133 /* DatabaseWriter.swift */, 5636E9BB1D22574100B9B05F /* FetchRequest.swift */, @@ -1669,8 +1674,8 @@ 56A238781B9C75030082EB20 /* Statement.swift */, 566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */, 560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */, - 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */, 5605F1471C672E4000235C62 /* Support */, + 566B91321FA4D3810012D5B0 /* TransactionObserver.swift */, ); path = Core; sourceTree = ""; @@ -2410,6 +2415,7 @@ 56BF6D3C1DEF47DA006039A3 /* Fixits-0-90-1.swift in Sources */, 56DAA2E11DE9C827006E10C8 /* Cursor.swift in Sources */, 5653EB0520944C7C00F46237 /* BelongsToAssociation.swift in Sources */, + 5644DE6F20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */, 565490BC1D5AE236005622CB /* DatabaseSchemaCache.swift in Sources */, 565490BA1D5AE236005622CB /* DatabaseQueue.swift in Sources */, 565490CD1D5AE252005622CB /* Date.swift in Sources */, @@ -2521,6 +2527,7 @@ 5653EB0D20944C7C00F46237 /* HasManyAssociation.swift in Sources */, 566AD8B51D5318F4002EC1A8 /* TableDefinition.swift in Sources */, 5698AD241DABAEFA0056AF8C /* FTS5WrapperTokenizer.swift in Sources */, + 5644DE6E20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */, C96C0F2C2084A459006B2981 /* SQLiteDateParser.swift in Sources */, 5605F1681C672E4000235C62 /* NSNumber.swift in Sources */, 56CEB5041EAA2F4D00BFAF62 /* FTS4.swift in Sources */, @@ -2974,6 +2981,7 @@ 566475A21D9810A400FF74B8 /* SQLSelectable+QueryInterface.swift in Sources */, 5674A6E41F307F0E0095F066 /* DatabaseValueConvertible+Decodable.swift in Sources */, 5653EB0C20944C7C00F46237 /* HasManyAssociation.swift in Sources */, + 5644DE6D20F8C32E001FFDDE /* DatabaseValueConversion.swift in Sources */, 5657AAB91D107001006283EF /* NSData.swift in Sources */, 560D92421C672C3E00F4F92B /* StatementColumnConvertible.swift in Sources */, 56CEB5531EAA359A00BFAF62 /* SQLExpression.swift in Sources */, diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 28962ef3b2..27a5c7480d 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -206,105 +206,6 @@ extension DatabaseValue { // MARK: - Lossless conversions -struct ValueConversionDebuggingInfo { - var _statement: SelectStatement? - var _row: Row? - var _columnIndex: Int? - var _columnName: String? - - var statement: SelectStatement? { - return _statement ?? _row?.statement - } - - var row: Row? { - return _row ?? _statement.map { Row(statement: $0) } - } - - var columnIndex: Int? { - if let columnIndex = _columnIndex { - return columnIndex - } - if let columnName = _columnName, let row = row { - return row.index(ofColumn: columnName) - } - return nil - } - - var columnName: String? { - if let columnName = _columnName { - return columnName - } - if let columnIndex = _columnIndex, let row = row { - let rowIndex = row.index(row.startIndex, offsetBy: columnIndex) - return row[rowIndex].0 - } - return nil - } - - init(statement: SelectStatement? = nil, row: Row? = nil, columnIndex: Int? = nil, columnName: String? = nil) { - _statement = statement - _row = row - _columnIndex = columnIndex - _columnName = columnName - } -} - -struct ValueConversionError: Error, CustomStringConvertible { - var dbValue: DatabaseValue - var debugInfo: ValueConversionDebuggingInfo - - var description: String { - var error = "could not convert database value \(dbValue) to \(T.self)" - var extras: [String] = [] - if let columnName = debugInfo.columnName { - extras.append("column: `\(columnName)`") - } - if let columnIndex = debugInfo.columnIndex { - extras.append("column index: \(columnIndex)") - } - if let row = debugInfo.row { - extras.append("row: \(row)") - } - if let statement = debugInfo.statement { - extras.append("statement: `\(statement.sql)`") - if statement.arguments.isEmpty == false { - extras.append("arguments: \(statement.arguments)") - } - } - if extras.isEmpty == false { - error += " (" + extras.joined(separator: ", ") + ")" - } - return error - } -} - -extension DatabaseValueConvertible { - /// Performs lossless conversion from a database value. - /// - /// - throws: ValueConversionError - static func convert(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { - if let value = fromDatabaseValue(dbValue) { - return value - } else { - throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) - } - } - - /// Performs lossless conversion from a database value. - /// - /// - throws: ValueConversionError - static func convertOptional(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self? { - // Use fromDatabaseValue before checking for null: this allows DatabaseValue to convert NULL to .null. - if let value = fromDatabaseValue(dbValue) { - return value - } else if dbValue.isNull { - return nil - } else { - throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) - } - } -} - extension DatabaseValue { /// Converts the database value to the type T. /// @@ -326,7 +227,7 @@ extension DatabaseValue { /// conversion error @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible { - return try! T.convert(from: self, debugInfo: ValueConversionDebuggingInfo()) + return require { try T.decode(from: self, debugInfo: ValueConversionDebuggingInfo()) } } /// Converts the database value to the type Optional. @@ -350,7 +251,7 @@ extension DatabaseValue { /// conversion error @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible { - return try! T.convertOptional(from: self, debugInfo: ValueConversionDebuggingInfo()) + return require { try T.decodeIfPresent(from: self, debugInfo: ValueConversionDebuggingInfo()) } } } diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift new file mode 100644 index 0000000000..1f604509b8 --- /dev/null +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -0,0 +1,180 @@ +// All primitive value conversion methods. + +/// An alternative to try!, preferred for conversion methods. +@inline(__always) +func require(file: StaticString = #file, line: UInt = #line, block: () throws -> T) -> T { + do { + return try block() + } catch { + fatalError(String(describing: error), file: file, line: line) + } +} + +/// A type that helps the user understanding value conversion errors +struct ValueConversionDebuggingInfo { + private var _statement: SelectStatement? + private var _row: Row? + private var _columnIndex: Int? + private var _columnName: String? + + init(statement: SelectStatement? = nil, row: Row? = nil, columnIndex: Int? = nil, columnName: String? = nil) { + _statement = statement + _row = row + _columnIndex = columnIndex + _columnName = columnName + } + + var statement: SelectStatement? { + return _statement ?? _row?.statement + } + + var row: Row? { + return _row ?? _statement.map { Row(statement: $0) } + } + + var columnIndex: Int? { + if let columnIndex = _columnIndex { + return columnIndex + } + if let columnName = _columnName, let row = row { + return row.index(ofColumn: columnName) + } + return nil + } + + var columnName: String? { + if let columnName = _columnName { + return columnName + } + if let columnIndex = _columnIndex, let row = row { + let rowIndex = row.index(row.startIndex, offsetBy: columnIndex) + return row[rowIndex].0 + } + return nil + } +} + +/// A conversion error +struct ValueConversionError: Error, CustomStringConvertible { + var dbValue: DatabaseValue + var debugInfo: ValueConversionDebuggingInfo + + var description: String { + var error = "could not convert database value \(dbValue) to \(T.self)" + var extras: [String] = [] + if let columnName = debugInfo.columnName { + extras.append("column: `\(columnName)`") + } + if let columnIndex = debugInfo.columnIndex { + extras.append("column index: \(columnIndex)") + } + if let row = debugInfo.row { + extras.append("row: \(row)") + } + if let statement = debugInfo.statement { + extras.append("statement: `\(statement.sql)`") + if statement.arguments.isEmpty == false { + extras.append("arguments: \(statement.arguments)") + } + } + if extras.isEmpty == false { + error += " (" + extras.joined(separator: ", ") + ")" + } + return error + } +} + +extension DatabaseValueConvertible { + /// Performs lossless conversion from a database value. + /// + /// - throws: ValueConversionError + static func decode(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { + if let value = fromDatabaseValue(dbValue) { + return value + } else { + throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) + } + } + + /// Performs lossless conversion from a database value. + /// + /// - throws: ValueConversionError + static func decodeIfPresent(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self? { + // Use fromDatabaseValue before checking for null: this allows DatabaseValue to convert NULL to .null. + if let value = fromDatabaseValue(dbValue) { + return value + } else if dbValue.isNull { + return nil + } else { + throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) + } + } +} + +extension DatabaseValueConvertible where Self: StatementColumnConvertible { + + /// Performs lossless conversion from a statement value. + /// + /// - throws: ValueConversionError + @inline(__always) + static func decode(from sqliteStatement: SQLiteStatement, index: Int32, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { + if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { + throw ValueConversionError(dbValue: .null, debugInfo: debugInfo()) + } + return self.init(sqliteStatement: sqliteStatement, index: index) + } + + /// Performs lossless conversion from a statement value. + @inline(__always) + static func decodeIfPresent(from sqliteStatement: SQLiteStatement, index: Int32) -> Self? { + if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { + return nil + } + return self.init(sqliteStatement: sqliteStatement, index: index) + } +} + +extension Row { + + @inline(__always) + func decodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + return try Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + } + + @inline(__always) + func decode( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + return try Value.decode(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + } + + @inline(__always) + func fastDecodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + { + if let sqliteStatement = sqliteStatement { + return Value.decodeIfPresent(from: sqliteStatement, index: Int32(index)) + } + return try impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } + + @inline(__always) + func fastDecode( + _ type: Value.Type, + atUncheckedIndex index: Int, + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + { + if let sqliteStatement = sqliteStatement { + return try Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + } + return try impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + } +} diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index 1ad76b5263..356b64172f 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -70,7 +70,7 @@ public final class DatabaseValueCursor : Cursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return try! Value.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) + return require { try Value.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) } case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) @@ -110,7 +110,7 @@ public final class NullableDatabaseValueCursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return try! Value.convertOptional(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) + return require { try Value.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) } case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index cbfab9bfe5..928f768517 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -132,52 +132,13 @@ extension Row { extension Row { - // MARK: - Extracting values: primitive + // MARK: - Extracting Values + @inline(__always) func index(ofColumn name: String) -> Int? { return impl.index(ofColumn: name) } - func decodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? - { - return try impl.decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - func decode( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - { - return try impl.decode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - func fastDecodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? - { - if let sqliteStatement = sqliteStatement { - return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) - } - return try impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - func fastDecode( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - { - if let sqliteStatement = sqliteStatement { - return try Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: debugInfo) - } - return try impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) - } - - // MARK: - Extracting Values - /// Returns true if and only if one column contains a non-null value, or if /// the row was fetched with a row adapter that defines a scoped row that /// contains a non-null value. @@ -234,7 +195,7 @@ extension Row { /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return require { try decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the value at given index, converted to the requested type. @@ -251,7 +212,7 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return require { try fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the value at given index, converted to the requested type. @@ -263,7 +224,7 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return require { try decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the value at given index, converted to the requested type. @@ -279,7 +240,7 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return require { try fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -315,7 +276,7 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return try! decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return require { try decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the value at given column, converted to the requested type. @@ -334,7 +295,7 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return try! fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return require { try fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the value at given column, converted to the requested type. @@ -351,7 +312,7 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return try! decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return require { try decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the value at given column, converted to the requested type. @@ -372,7 +333,7 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return try! fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return require { try fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -456,7 +417,7 @@ extension Row { /// than the row's lifetime. public func dataNoCopy(atIndex index: Int) -> Data? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return try! impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + return require { try impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the optional Data at given column. @@ -474,7 +435,7 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return try! impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + return require { try impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the optional `NSData` at given column. @@ -1250,29 +1211,13 @@ extension RowImpl { return databaseValue(atUncheckedIndex: index).isNull } - func decode( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - { - return try Value.convert(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) - } - - func decodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? - { - return try Value.convertOptional(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) - } - func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value { - // default fast implementation is slow - return try decode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + // default implementation is slow + return try Value.decode(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } func fastDecodeIfPresent( @@ -1280,12 +1225,13 @@ extension RowImpl { atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? { - // default fast implementation is slow - return try decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + // default implementation is slow + return try Value.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { - return try Data.convertOptional(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + // default implementation copies + return try Data.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } } @@ -1404,14 +1350,14 @@ private struct StatementRowImpl : RowImpl { atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value { - return try Value.convert(sqliteStatement: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + return try Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) } func fastOptionalValue( atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? { - return Value.convertOptional(sqliteStatement: sqliteStatement, index: Int32(index)) + return Value.decodeIfPresent(from: sqliteStatement, index: Int32(index)) } func columnName(atUncheckedIndex index: Int) -> String { diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 141da0764e..abf1f34a60 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -50,30 +50,6 @@ public protocol StatementColumnConvertible { init(sqliteStatement: SQLiteStatement, index: Int32) } -extension DatabaseValueConvertible where Self: StatementColumnConvertible { - - /// Performs lossless conversion from a statement value. - /// - /// - throws: ValueConversionError - @inline(__always) - static func convert(sqliteStatement: SQLiteStatement, index: Int32, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { - if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { - throw ValueConversionError(dbValue: .null, debugInfo: debugInfo()) - } - return self.init(sqliteStatement: sqliteStatement, index: index) - } - - /// Performs lossless conversion from a statement value. - @inline(__always) - static func convertOptional(sqliteStatement: SQLiteStatement, index: Int32) -> Self? { - if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { - return nil - } - return self.init(sqliteStatement: sqliteStatement, index: index) - } -} - - /// A cursor of database values extracted from a single column. /// For example: /// @@ -105,7 +81,7 @@ public final class ColumnCursor Bool { return try! Bool.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int.Type) throws -> Int { return try! Int.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int8.Type) throws -> Int8 { return try! Int8.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int16.Type) throws -> Int16 { return try! Int16.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int32.Type) throws -> Int32 { return try! Int32.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int64.Type) throws -> Int64 { return try! Int64.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt.Type) throws -> UInt { return try! UInt.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt8.Type) throws -> UInt8 { return try! UInt8.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt16.Type) throws -> UInt16 { return try! UInt16.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt32.Type) throws -> UInt32 { return try! UInt32.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt64.Type) throws -> UInt64 { return try! UInt64.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Float.Type) throws -> Float { return try! Float.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Double.Type) throws -> Double { return try! Double.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: String.Type) throws -> String { return try! String.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Bool.Type) throws -> Bool { return require { try Bool.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Int.Type) throws -> Int { return require { try Int.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Int8.Type) throws -> Int8 { return require { try Int8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Int16.Type) throws -> Int16 { return require { try Int16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Int32.Type) throws -> Int32 { return require { try Int32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Int64.Type) throws -> Int64 { return require { try Int64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: UInt.Type) throws -> UInt { return require { try UInt.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: UInt8.Type) throws -> UInt8 { return require { try UInt8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: UInt16.Type) throws -> UInt16 { return require { try UInt16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: UInt32.Type) throws -> UInt32 { return require { try UInt32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: UInt64.Type) throws -> UInt64 { return require { try UInt64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Float.Type) throws -> Float { return require { try Float.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Double.Type) throws -> Double { return require { try Double.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: String.Type) throws -> String { return require { try String.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } /// Decodes a single value of the given type. /// @@ -41,7 +41,7 @@ private struct DatabaseValueDecodingContainer: SingleValueDecodingContainer { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows custom database decoding, such as decoding Date from // String, for example. - return try! type.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) as! T + return require { try type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) as! T } } else { return try T(from: DatabaseValueDecoder(dbValue: dbValue, codingPath: codingPath)) } diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 301fda70a9..450aab87e7 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -75,7 +75,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return try! type.convertOptional(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T? + return require { try type.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T? } } else if dbValue.isNull { return nil } else { @@ -114,7 +114,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return try! type.convert(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T + return require { try type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T } } else { return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } @@ -227,7 +227,7 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return try! type.convert(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(row: row, columnName: column.stringValue)) as! T + return require { try type.decode(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(row: row, columnName: column.stringValue)) as! T } } else { return try T(from: RowDecoder(row: row, codingPath: [column])) } diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index 81603aead1..1a04611053 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -185,6 +185,8 @@ 564448851EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; 564448881EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; 564448891EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; + 5644DE7C20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */; }; + 5644DE7D20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */; }; 564E73EF203DA2A2000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73EB203DA29B000C443C /* JoinSupportTests.swift */; }; 564E73F0203DA2A3000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73EB203DA29B000C443C /* JoinSupportTests.swift */; }; 564E73F1203DA2A3000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73EB203DA29B000C443C /* JoinSupportTests.swift */; }; @@ -961,6 +963,7 @@ 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionObserverSavepointsTests.swift; sourceTree = ""; }; 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = ""; }; + 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; }; 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; }; 564E73EB203DA29B000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; @@ -1709,6 +1712,7 @@ 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */, 566A842720413D6E00E50BFD /* DatabaseSnapshot.swift */, 56A238751B9C75030082EB20 /* DatabaseValue.swift */, + 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */, 560D923E1C672C3E00F4F92B /* DatabaseValueConvertible.swift */, 563363C31C942C37000BE133 /* DatabaseWriter.swift */, 5636E9BB1D22574100B9B05F /* FetchRequest.swift */, @@ -2200,6 +2204,7 @@ 560FC5311CB003810014AA8E /* TableRecord.swift in Sources */, 56DAA2DC1DE9C827006E10C8 /* Cursor.swift in Sources */, 5674A6F11F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, + 5644DE7C20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */, 560FC5321CB003810014AA8E /* DatabasePool.swift in Sources */, 560FC5331CB003810014AA8E /* Migration.swift in Sources */, 560FC5341CB003810014AA8E /* QueryInterfaceQuery.swift in Sources */, @@ -2635,6 +2640,7 @@ 56AFCA061CB1A8BB00F48B96 /* Configuration.swift in Sources */, 56DAA2DF1DE9C827006E10C8 /* Cursor.swift in Sources */, 5674A6EE1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, + 5644DE7D20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */, 56AFCA071CB1A8BB00F48B96 /* DatabasePool.swift in Sources */, 56AFCA081CB1A8BB00F48B96 /* Statement.swift in Sources */, 56AFCA091CB1A8BB00F48B96 /* QueryInterfaceQuery.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index 1880e46011..b4393c9987 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -45,6 +45,8 @@ 5636E9C11D22574100B9B05F /* FetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5636E9BB1D22574100B9B05F /* FetchRequest.swift */; }; 564448861EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; 5644488A1EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; + 5644DE7820F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */; }; + 5644DE7920F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */; }; 564E73F3203DA2AC000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73E7203DA278000C443C /* JoinSupportTests.swift */; }; 564E73F4203DA2AD000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73E7203DA278000C443C /* JoinSupportTests.swift */; }; 564F9C211F069B4E00877A00 /* DatabaseAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */; }; @@ -632,6 +634,7 @@ 5634B1061CF9B970005360B9 /* TransactionObserverSavepointsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionObserverSavepointsTests.swift; sourceTree = ""; }; 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = ""; }; + 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; }; 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; }; 564E73E7203DA278000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; @@ -1353,6 +1356,7 @@ 5695311E1C907A8C00CF1A2B /* DatabaseSchemaCache.swift */, 566A842B20413D9A00E50BFD /* DatabaseSnapshot.swift */, 56A238751B9C75030082EB20 /* DatabaseValue.swift */, + 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */, 560D923E1C672C3E00F4F92B /* DatabaseValueConvertible.swift */, 563363C31C942C37000BE133 /* DatabaseWriter.swift */, 5636E9BB1D22574100B9B05F /* FetchRequest.swift */, @@ -1873,6 +1877,7 @@ F3BA801D1CFB288C003DC1BA /* DatabaseDateComponents.swift in Sources */, 5674A6ED1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, 5657AB141D10899D006283EF /* URL.swift in Sources */, + 5644DE7920F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */, 56DAA2E01DE9C827006E10C8 /* Cursor.swift in Sources */, F3BA800C1CFB286F003DC1BA /* DatabaseError.swift in Sources */, 566B91301FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, @@ -2146,6 +2151,7 @@ F3BA80791CFB2E61003DC1BA /* DatabaseDateComponents.swift in Sources */, 5674A6EC1F307F0E0095F066 /* DatabaseValueConvertible+Encodable.swift in Sources */, 5657AB111D10899D006283EF /* URL.swift in Sources */, + 5644DE7820F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */, 56DAA2DD1DE9C827006E10C8 /* Cursor.swift in Sources */, F3BA80681CFB2E55003DC1BA /* DatabaseError.swift in Sources */, 566B912D1FA4D0CC0012D5B0 /* StatementAuthorizer.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift index 28dd7226aa..0b47edb4eb 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift @@ -43,54 +43,54 @@ class DatabaseValueConvertibleErrorTests: GRDBTestCase { // TODO: find a way to turn those into real tests let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in - let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") - statement.arguments = ["invalid"] - let row = try Row.fetchOne(statement)! - - // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) - _ = Record1(row: row) - - // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try Record1.fetchOne(statement) - - // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) - _ = Record2(row: row) - - // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try Record2.fetchOne(statement) - - // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) - _ = Record3(row: row) - - // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try Record3.fetchOne(statement) - - // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) - _ = Record4(row: row) - - // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try Record4.fetchOne(statement) - - // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try String.fetchAll(statement) - - // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) - _ = row["name"] as String - - // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"]) - _ = row[0] as String - - // could not convert database value NULL to Value1 (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try Value1.fetchAll(statement) - - // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) - _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) - - // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"foo"]) - _ = row["name"] as Value1 - - // could not convert database value NULL to Value1 (index: 0, row: [name:NULL team:"foo"]) - _ = row[0] as Value1 +// let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") +// statement.arguments = ["invalid"] +// let row = try Row.fetchOne(statement)! +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) +// _ = Record1(row: row) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try Record1.fetchOne(statement) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) +// _ = Record2(row: row) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try Record2.fetchOne(statement) +// +// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) +// _ = Record3(row: row) +// +// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try Record3.fetchOne(statement) +// +// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) +// _ = Record4(row: row) +// +// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try Record4.fetchOne(statement) +// +// // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try String.fetchAll(statement) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) +// _ = row["name"] as String +// +// // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"]) +// _ = row[0] as String +// +// // could not convert database value NULL to Value1 (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try Value1.fetchAll(statement) +// +// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) +// +// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"foo"]) +// _ = row["name"] as Value1 +// +// // could not convert database value NULL to Value1 (index: 0, row: [name:NULL team:"foo"]) +// _ = row[0] as Value1 } } } From e9e1fa85c68dd6d81a4d0c92f68cf001c44d3c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 13 Jul 2018 14:26:23 +0200 Subject: [PATCH 05/31] Cleanup --- GRDB.xcodeproj/project.pbxproj | 14 ++--- GRDB/Core/Row.swift | 48 +++++++++++++---- GRDBCipher.xcodeproj/project.pbxproj | 10 ++++ GRDBCustom.xcodeproj/project.pbxproj | 6 +++ ...> DatabaseValueConversionErrorTests.swift} | 54 +++++++++---------- 5 files changed, 88 insertions(+), 44 deletions(-) rename Tests/GRDBTests/{DatabaseValueConvertibleErrorTests.swift => DatabaseValueConversionErrorTests.swift} (54%) diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index c4a6f4065a..5678acb5a3 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -151,8 +151,8 @@ 564F9C2D1F075DD200877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; 564F9C311F07611600877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; 564F9C341F07611900877A00 /* DatabaseFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */; }; - 564FCE5E20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */; }; - 564FCE5F20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */; }; + 564FCE5E20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564FCE5D20F7E11A00202B90 /* DatabaseValueConversionErrorTests.swift */; }; + 564FCE5F20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564FCE5D20F7E11A00202B90 /* DatabaseValueConversionErrorTests.swift */; }; 5653EAD620944B4F00F46237 /* AssociationChainRowScopesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */; }; 5653EAD720944B4F00F46237 /* AssociationChainRowScopesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */; }; 5653EAD820944B4F00F46237 /* AssociationBelongsToRowScopeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5653EAC720944B4C00F46237 /* AssociationBelongsToRowScopeTests.swift */; }; @@ -875,7 +875,7 @@ 564E73DE203D50B9000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; 564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseFunction.swift; sourceTree = ""; }; - 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConvertibleErrorTests.swift; sourceTree = ""; }; + 564FCE5D20F7E11A00202B90 /* DatabaseValueConversionErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversionErrorTests.swift; sourceTree = ""; }; 5653EAC620944B4C00F46237 /* AssociationChainRowScopesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationChainRowScopesTests.swift; sourceTree = ""; }; 5653EAC720944B4C00F46237 /* AssociationBelongsToRowScopeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationBelongsToRowScopeTests.swift; sourceTree = ""; }; 5653EAC820944B4D00F46237 /* AssociationRowScopeSearchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssociationRowScopeSearchTests.swift; sourceTree = ""; }; @@ -1288,11 +1288,11 @@ 56176C581EACC2D8000F3F2B /* GRDBTests */ = { isa = PBXGroup; children = ( - 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, 562EA81E1F17B26F00FA528C /* Compilation */, 56A238111B9C74A90082EB20 /* Core */, 5698AC3E1DA2BEBB0056AF8C /* FTS */, 56176CA01EACEE2A000F3F2B /* GRDBCipher */, + 5623E0901B4AFACC00B20B7F /* GRDBTestCase.swift */, 56A238231B9C74A90082EB20 /* Migrations */, 569978D31B539038005EBEED /* Private */, 56300B5C1C53C38F005A543B /* QueryInterface */, @@ -1566,6 +1566,7 @@ 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */, 566A8424204120B700E50BFD /* DatabaseSnapshotTests.swift */, 56A238131B9C74A90082EB20 /* DatabaseTests.swift */, + 564FCE5D20F7E11A00202B90 /* DatabaseValueConversionErrorTests.swift */, 56A2381B1B9C74A90082EB20 /* DatabaseValueConversionTests.swift */, 56A238191B9C74A90082EB20 /* DatabaseValueConvertible */, 56A238181B9C74A90082EB20 /* DatabaseValueTests.swift */, @@ -1591,7 +1592,6 @@ children = ( 5674A70A1F3087700095F066 /* DatabaseValueConvertibleDecodableTests.swift */, 5674A70B1F3087700095F066 /* DatabaseValueConvertibleEncodableTests.swift */, - 564FCE5D20F7E11A00202B90 /* DatabaseValueConvertibleErrorTests.swift */, 56A238B51B9CA2590082EB20 /* DatabaseTimestampTests.swift */, 56E8CE0C1BB4FA5600828BEC /* DatabaseValueConvertibleFetchTests.swift */, 56A2381A1B9C74A90082EB20 /* DatabaseValueConvertibleSubclassTests.swift */, @@ -2614,7 +2614,7 @@ 567F45AC1F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, 5657AB621D108BA9006283EF /* FoundationNSURLTests.swift in Sources */, 5657AB6A1D108BA9006283EF /* FoundationURLTests.swift in Sources */, - 564FCE5F20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */, + 564FCE5F20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */, 5653EADF20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */, 56F3E74D1E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */, 56EB0AB31BCD787300A3DC55 /* DataMemoryTests.swift in Sources */, @@ -2777,7 +2777,7 @@ 56D496601D81304E008276D7 /* FoundationNSUUIDTests.swift in Sources */, 567F45A81F888B2600030B59 /* TruncateOptimizationTests.swift in Sources */, 56D4968D1D81316E008276D7 /* DatabaseFunctionTests.swift in Sources */, - 564FCE5E20F7E11B00202B90 /* DatabaseValueConvertibleErrorTests.swift in Sources */, + 564FCE5E20F7E11B00202B90 /* DatabaseValueConversionErrorTests.swift in Sources */, 5653EADE20944B4F00F46237 /* AssociationHasOneSQLDerivationTests.swift in Sources */, 56D496661D813086008276D7 /* QueryInterfaceRequestTests.swift in Sources */, 56F3E7491E66F83A00BF0F01 /* ResultCodeTests.swift in Sources */, diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 928f768517..512f767f0e 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -195,7 +195,10 @@ extension Row { /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + return require { try decodeIfPresent( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the value at given index, converted to the requested type. @@ -212,7 +215,10 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + return require { try fastDecodeIfPresent( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the value at given index, converted to the requested type. @@ -224,7 +230,10 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + return require { try decode( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the value at given index, converted to the requested type. @@ -240,7 +249,10 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + return require { try fastDecode( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -276,7 +288,10 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return require { try decodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + return require { try decodeIfPresent( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the value at given column, converted to the requested type. @@ -295,7 +310,10 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return require { try fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + return require { try fastDecodeIfPresent( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the value at given column, converted to the requested type. @@ -312,7 +330,10 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return require { try decode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + return require { try decode( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the value at given column, converted to the requested type. @@ -333,7 +354,10 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return require { try fastDecode(Value.self, atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + return require { try fastDecode( + Value.self, + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -417,7 +441,9 @@ extension Row { /// than the row's lifetime. public func dataNoCopy(atIndex index: Int) -> Data? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + return require { try impl.dataNoCopy( + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } } /// Returns the optional Data at given column. @@ -435,7 +461,9 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return require { try impl.dataNoCopy(atUncheckedIndex: index, debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + return require { try impl.dataNoCopy( + atUncheckedIndex: index, + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } } /// Returns the optional `NSData` at given column. diff --git a/GRDBCipher.xcodeproj/project.pbxproj b/GRDBCipher.xcodeproj/project.pbxproj index 1a04611053..04f6e7ab9d 100755 --- a/GRDBCipher.xcodeproj/project.pbxproj +++ b/GRDBCipher.xcodeproj/project.pbxproj @@ -187,6 +187,10 @@ 564448891EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; 5644DE7C20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */; }; 5644DE7D20F8C903001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */; }; + 5644DE8220F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; + 5644DE8320F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; + 5644DE8420F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; + 5644DE8520F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; 564E73EF203DA2A2000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73EB203DA29B000C443C /* JoinSupportTests.swift */; }; 564E73F0203DA2A3000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73EB203DA29B000C443C /* JoinSupportTests.swift */; }; 564E73F1203DA2A3000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73EB203DA29B000C443C /* JoinSupportTests.swift */; }; @@ -964,6 +968,7 @@ 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = ""; }; 5644DE7A20F8C903001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; }; + 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversionErrorTests.swift; sourceTree = ""; }; 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; }; 564E73EB203DA29B000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; @@ -1616,6 +1621,7 @@ 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */, 566A842F20413DCF00E50BFD /* DatabaseSnapshotTests.swift */, 56A238131B9C74A90082EB20 /* DatabaseTests.swift */, + 5644DE8120F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift */, 56A2381B1B9C74A90082EB20 /* DatabaseValueConversionTests.swift */, 56A238191B9C74A90082EB20 /* DatabaseValueConvertible */, 56A238181B9C74A90082EB20 /* DatabaseValueTests.swift */, @@ -2354,6 +2360,7 @@ 560FC58F1CB00B880014AA8E /* DatabaseReaderTests.swift in Sources */, 5653EBD020961FE800F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 560FC5901CB00B880014AA8E /* RecordEditedTests.swift in Sources */, + 5644DE8220F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */, 560FC5911CB00B880014AA8E /* RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift in Sources */, 56CC924C201E059100CB597E /* DropFirstCursorTests.swift in Sources */, 562393311DEDFC5700A6B01F /* AnyCursorTests.swift in Sources */, @@ -2516,6 +2523,7 @@ 567156501CB16729007DC145 /* RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift in Sources */, 5653EBD120961FE800F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 567156511CB16729007DC145 /* FetchableRecordTests.swift in Sources */, + 5644DE8320F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */, 56A8C2441D1918EE0096E9D4 /* FoundationUUIDTests.swift in Sources */, 56CC924D201E059100CB597E /* DropFirstCursorTests.swift in Sources */, 562393321DEDFC5700A6B01F /* AnyCursorTests.swift in Sources */, @@ -2790,6 +2798,7 @@ 56A8C2491D1918F10096E9D4 /* FoundationNSUUIDTests.swift in Sources */, 5653EBD220961FE800F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 56AFCA5E1CB1AA9900F48B96 /* FetchableRecordTests.swift in Sources */, + 5644DE8420F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */, 56AFCA5F1CB1AA9900F48B96 /* DatabaseValueConvertibleFetchTests.swift in Sources */, 56AFCA601CB1AA9900F48B96 /* RecordPrimaryKeyRowIDTests.swift in Sources */, 56CC924E201E059100CB597E /* DropFirstCursorTests.swift in Sources */, @@ -2952,6 +2961,7 @@ 5657AB3C1D108BA9006283EF /* FoundationDataTests.swift in Sources */, 5653EBD320961FE800F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 5634B10C1CF9B970005360B9 /* TransactionObserverSavepointsTests.swift in Sources */, + 5644DE8520F8D1E4001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */, 56AFCAB51CB1ABC800F48B96 /* DatabaseQueueReadOnlyTests.swift in Sources */, 56AFCAB61CB1ABC800F48B96 /* DatabaseReaderTests.swift in Sources */, 56CC924F201E059100CB597E /* DropFirstCursorTests.swift in Sources */, diff --git a/GRDBCustom.xcodeproj/project.pbxproj b/GRDBCustom.xcodeproj/project.pbxproj index b4393c9987..998fa7c85b 100755 --- a/GRDBCustom.xcodeproj/project.pbxproj +++ b/GRDBCustom.xcodeproj/project.pbxproj @@ -47,6 +47,8 @@ 5644488A1EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */; }; 5644DE7820F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */; }; 5644DE7920F8C8EA001FFDDE /* DatabaseValueConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */; }; + 5644DE7F20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; + 5644DE8020F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */; }; 564E73F3203DA2AC000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73E7203DA278000C443C /* JoinSupportTests.swift */; }; 564E73F4203DA2AD000C443C /* JoinSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564E73E7203DA278000C443C /* JoinSupportTests.swift */; }; 564F9C211F069B4E00877A00 /* DatabaseAggregateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */; }; @@ -635,6 +637,7 @@ 5636E9BB1D22574100B9B05F /* FetchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRequest.swift; sourceTree = ""; }; 564448821EF56B1B00DD2861 /* DatabaseAfterNextTransactionCommitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAfterNextTransactionCommitTests.swift; sourceTree = ""; }; 5644DE7620F8C8E9001FFDDE /* DatabaseValueConversion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversion.swift; sourceTree = ""; }; + 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseValueConversionErrorTests.swift; sourceTree = ""; }; 564A50C61BFF4B7F00B3A3A2 /* DatabaseCollationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseCollationTests.swift; sourceTree = ""; }; 564E73E7203DA278000C443C /* JoinSupportTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinSupportTests.swift; sourceTree = ""; }; 564F9C1D1F069B4E00877A00 /* DatabaseAggregateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatabaseAggregateTests.swift; sourceTree = ""; }; @@ -1260,6 +1263,7 @@ 56C3F7521CF9F12400F6A361 /* DatabaseSavepointTests.swift */, 566A843420413DE400E50BFD /* DatabaseSnapshotTests.swift */, 56A238131B9C74A90082EB20 /* DatabaseTests.swift */, + 5644DE7E20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift */, 56A2381B1B9C74A90082EB20 /* DatabaseValueConversionTests.swift */, 56A238191B9C74A90082EB20 /* DatabaseValueConvertible */, 56A238181B9C74A90082EB20 /* DatabaseValueTests.swift */, @@ -2027,6 +2031,7 @@ F3BA80D61CFB2FFD003DC1BA /* DatabaseReaderTests.swift in Sources */, 5653EB7520961FB200F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 5698AC9D1DA4B0430056AF8C /* FTS4RecordTests.swift in Sources */, + 5644DE8020F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */, 56A4CDB71D4234B200B1A9B9 /* SQLExpressionLiteralTests.swift in Sources */, 56CC924A201E058900CB597E /* DropFirstCursorTests.swift in Sources */, 56B964D21DA521450002DA19 /* FTS5RecordTests.swift in Sources */, @@ -2301,6 +2306,7 @@ F3BA81321CFB3064003DC1BA /* RecordPrimaryKeySingleWithReplaceConflictResolutionTests.swift in Sources */, 5653EB7420961FB200F46237 /* AssociationRowScopeSearchTests.swift in Sources */, 5623935A1DEE013C00A6B01F /* FilterCursorTests.swift in Sources */, + 5644DE7F20F8D1D1001FFDDE /* DatabaseValueConversionErrorTests.swift in Sources */, F3BA80FC1CFB3021003DC1BA /* UpdateStatementTests.swift in Sources */, 56CC9249201E058900CB597E /* DropFirstCursorTests.swift in Sources */, 5623936C1DEE0CD200A6B01F /* FlattenCursorTests.swift in Sources */, diff --git a/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift similarity index 54% rename from Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift rename to Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index 0b47edb4eb..ccb0031cc1 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -7,7 +7,7 @@ import XCTest @testable import GRDB #endif -class DatabaseValueConvertibleErrorTests: GRDBTestCase { +class DatabaseValueConversionErrorTests: GRDBTestCase { struct Record1: Codable, FetchableRecord { var name: String var team: String @@ -40,57 +40,57 @@ class DatabaseValueConvertibleErrorTests: GRDBTestCase { } func testError() throws { - // TODO: find a way to turn those into real tests - let dbQueue = try makeDatabaseQueue() - try dbQueue.read { db in +// // TODO: find a way to turn those into real tests +// let dbQueue = try makeDatabaseQueue() +// try dbQueue.read { db in // let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") // statement.arguments = ["invalid"] // let row = try Row.fetchOne(statement)! // // // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = Record1(row: row) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try Record1.fetchOne(statement) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = Record2(row: row) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try Record2.fetchOne(statement) -// -// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) +// +// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"]) // _ = Record3(row: row) // -// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try Record3.fetchOne(statement) // -// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"]) +// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"]) // _ = Record4(row: row) // -// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try Record4.fetchOne(statement) // -// // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try String.fetchAll(statement) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"foo"]) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = row["name"] as String -// -// // could not convert database value NULL to String (column index: 0, row: [name:NULL team:"foo"]) +// +// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = row[0] as String -// -// // could not convert database value NULL to Value1 (column index: 0, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// +// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try Value1.fetchAll(statement) -// -// // could not convert database value "foo" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"foo"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["foo"]) +// +// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) // _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) // -// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"foo"]) +// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = row["name"] as Value1 // -// // could not convert database value NULL to Value1 (index: 0, row: [name:NULL team:"foo"]) +// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = row[0] as Value1 - } +// } } } From 9f2ba7245bfba116f725fc60059b338cdfb7d384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Jul 2018 09:38:50 +0200 Subject: [PATCH 06/31] fix SPM build --- GRDB/Core/DatabaseValueConversion.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 1f604509b8..24231c0903 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -1,3 +1,9 @@ +#if SWIFT_PACKAGE + import CSQLite +#elseif !GRDBCUSTOMSQLITE && !GRDBCIPHER + import SQLite3 +#endif + // All primitive value conversion methods. /// An alternative to try!, preferred for conversion methods. From b307e907cb7cc096fa45ac91de2120eac8d7020c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Jul 2018 10:53:17 +0200 Subject: [PATCH 07/31] CHANGELOG --- CHANGELOG.md | 12 ++++++++++++ SQLCipher/src | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9c57b083..9a23b52901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,20 @@ Release Notes ## Next Version +- [#384](https://github.com/groue/GRDB.swift/pull/384): Improve database value decoding diagnostics - [#393](https://github.com/groue/GRDB.swift/pull/393): Upgrade SQLCipher to 3.4.2, enable FTS5 on GRDBCipher and new pod GRDBPlus. +### API diff + +```diff + extension DatabaseValue { ++ @available(*, deprecated) + func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T ++ @available(*, deprecated) + func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? + } +``` + ### Documentation Diff - [Enabling FTS5 Support](README.md#enabling-fts5-support): Procedure for enabling FTS5 support in GRDB diff --git a/SQLCipher/src b/SQLCipher/src index 4f5c99d129..be3ac49760 160000 --- a/SQLCipher/src +++ b/SQLCipher/src @@ -1 +1 @@ -Subproject commit 4f5c99d129f7696c0ba29ab0911b1a56af46534a +Subproject commit be3ac497606a19f6cb18828e923772416afce2f5 From 78a6f7251b4677075eacd374699b4712b2fedd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 14 Jul 2018 11:32:24 +0200 Subject: [PATCH 08/31] Spot a few Demeter law violations --- GRDB/Core/RowAdapter.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 9351440cbc..0f68e839fc 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -493,12 +493,12 @@ struct AdaptedRowImpl : RowImpl { func hasNull(atUncheckedIndex index: Int) -> Bool { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.hasNull(atUncheckedIndex: mappedIndex) + return base.impl.hasNull(atUncheckedIndex: mappedIndex) // base.impl: Demeter violation } func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.databaseValue(atUncheckedIndex: mappedIndex) + return base.impl.databaseValue(atUncheckedIndex: mappedIndex) // base.impl: Demeter violation } func fastDecode( @@ -507,7 +507,7 @@ struct AdaptedRowImpl : RowImpl { debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) + return try base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } func fastDecodeIfPresent( @@ -516,7 +516,7 @@ struct AdaptedRowImpl : RowImpl { debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) + return try base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } func dataNoCopy( @@ -524,7 +524,7 @@ struct AdaptedRowImpl : RowImpl { debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) + return try base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } func columnName(atUncheckedIndex index: Int) -> String { From 89d5bd79459d9aa107b5bd7a1ee18c4f58b9b9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Tue, 7 Aug 2018 19:44:38 +0200 Subject: [PATCH 09/31] Restore SQLCipher/src to v3.4.2 --- SQLCipher/src | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SQLCipher/src b/SQLCipher/src index be3ac49760..f18dee2e56 160000 --- a/SQLCipher/src +++ b/SQLCipher/src @@ -1 +1 @@ -Subproject commit be3ac497606a19f6cb18828e923772416afce2f5 +Subproject commit f18dee2e566f191899027057e11500815f41ba1b From bcc45dc076d8075cd87225f7f671f2ccc9e2bb77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 9 Aug 2018 08:51:50 +0200 Subject: [PATCH 10/31] no more throwing conversion methods, and refactor ValueConversionDebuggingInfo --- GRDB/Core/DatabaseValue.swift | 4 +- GRDB/Core/DatabaseValueConversion.swift | 162 +++++++++--------- GRDB/Core/DatabaseValueConvertible.swift | 20 ++- GRDB/Core/Row.swift | 66 +++---- GRDB/Core/RowAdapter.swift | 12 +- GRDB/Core/StatementColumnConvertible.swift | 5 +- .../DatabaseValueConvertible+Decodable.swift | 30 ++-- GRDB/Record/FetchableRecord+Decodable.swift | 6 +- 8 files changed, 160 insertions(+), 145 deletions(-) diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 27a5c7480d..6e724ea0c5 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -227,7 +227,7 @@ extension DatabaseValue { /// conversion error @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible { - return require { try T.decode(from: self, debugInfo: ValueConversionDebuggingInfo()) } + return T.decode(from: self, debugInfo: ValueConversionDebuggingInfo()) } /// Converts the database value to the type Optional. @@ -251,7 +251,7 @@ extension DatabaseValue { /// conversion error @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible { - return require { try T.decodeIfPresent(from: self, debugInfo: ValueConversionDebuggingInfo()) } + return T.decodeIfPresent(from: self, debugInfo: ValueConversionDebuggingInfo()) } } diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 24231c0903..5ee4f7266f 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -6,113 +6,115 @@ // All primitive value conversion methods. -/// An alternative to try!, preferred for conversion methods. -@inline(__always) -func require(file: StaticString = #file, line: UInt = #line, block: () throws -> T) -> T { - do { - return try block() - } catch { - fatalError(String(describing: error), file: file, line: line) - } -} - /// A type that helps the user understanding value conversion errors struct ValueConversionDebuggingInfo { - private var _statement: SelectStatement? - private var _row: Row? - private var _columnIndex: Int? - private var _columnName: String? - + enum Source { + case statement(SelectStatement) + case sql(String, StatementArguments) + case row(Row) + } + enum Column { + case columnIndex(Int) + case columnName(String) + } + private var source: Source? + private var column: Column? + init(statement: SelectStatement? = nil, row: Row? = nil, columnIndex: Int? = nil, columnName: String? = nil) { - _statement = statement - _row = row - _columnIndex = columnIndex - _columnName = columnName + fatalError("not implemented") + } + + var sql: String? { + guard let source = source else { return nil } + switch source { + case .statement(let statement): return statement.sql + case .row(let row): return row.statement?.sql + case .sql(let sql, _): return sql + } } - var statement: SelectStatement? { - return _statement ?? _row?.statement + var arguments: StatementArguments? { + guard let source = source else { return nil } + switch source { + case .statement(let statement): return statement.arguments + case .row(let row): return row.statement?.arguments + case .sql(_, let arguments): return arguments + } } var row: Row? { - return _row ?? _statement.map { Row(statement: $0) } + guard let source = source else { return nil } + switch source { + case .statement(let statement): return Row(statement: statement) + case .row(let row): return row + case .sql: return nil + } } var columnIndex: Int? { - if let columnIndex = _columnIndex { - return columnIndex + guard let column = column else { return nil } + switch column { + case .columnIndex(let index): return index + case .columnName(let name): return row?.index(ofColumn: name) } - if let columnName = _columnName, let row = row { - return row.index(ofColumn: columnName) - } - return nil } var columnName: String? { - if let columnName = _columnName { - return columnName - } - if let columnIndex = _columnIndex, let row = row { - let rowIndex = row.index(row.startIndex, offsetBy: columnIndex) + guard let column = column else { return nil } + switch column { + case .columnIndex(let index): + guard let row = row else { return nil } + let rowIndex = row.index(row.startIndex, offsetBy: index) return row[rowIndex].0 + case .columnName(let name): return name } - return nil } } -/// A conversion error -struct ValueConversionError: Error, CustomStringConvertible { - var dbValue: DatabaseValue - var debugInfo: ValueConversionDebuggingInfo - - var description: String { - var error = "could not convert database value \(dbValue) to \(T.self)" - var extras: [String] = [] - if let columnName = debugInfo.columnName { - extras.append("column: `\(columnName)`") - } - if let columnIndex = debugInfo.columnIndex { - extras.append("column index: \(columnIndex)") - } - if let row = debugInfo.row { - extras.append("row: \(row)") - } - if let statement = debugInfo.statement { - extras.append("statement: `\(statement.sql)`") - if statement.arguments.isEmpty == false { - extras.append("arguments: \(statement.arguments)") - } - } - if extras.isEmpty == false { - error += " (" + extras.joined(separator: ", ") + ")" +/// The canonical conversion fatal error +func fatalConversionError(to: T.Type, from dbValue: DatabaseValue, debugInfo: ValueConversionDebuggingInfo) -> Never { + var message = "could not convert database value \(dbValue) to \(T.self)" + var extras: [String] = [] + if let columnName = debugInfo.columnName { + extras.append("column: `\(columnName)`") + } + if let columnIndex = debugInfo.columnIndex { + extras.append("column index: \(columnIndex)") + } + if let row = debugInfo.row { + extras.append("row: \(row)") + } + if let sql = debugInfo.sql { + extras.append("statement: `\(sql)`") + if let arguments = debugInfo.arguments, arguments.isEmpty == false { + extras.append("arguments: \(arguments)") } - return error } + if extras.isEmpty == false { + message += " (" + extras.joined(separator: ", ") + ")" + } + fatalError(message) } extension DatabaseValueConvertible { /// Performs lossless conversion from a database value. - /// - /// - throws: ValueConversionError - static func decode(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { + static func decode(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Self { if let value = fromDatabaseValue(dbValue) { return value } else { - throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) + fatalConversionError(to: Self.self, from: dbValue, debugInfo: debugInfo()) } } /// Performs lossless conversion from a database value. - /// - /// - throws: ValueConversionError - static func decodeIfPresent(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self? { + static func decodeIfPresent(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Self? { // Use fromDatabaseValue before checking for null: this allows DatabaseValue to convert NULL to .null. if let value = fromDatabaseValue(dbValue) { return value } else if dbValue.isNull { return nil } else { - throw ValueConversionError(dbValue: dbValue, debugInfo: debugInfo()) + fatalConversionError(to: Self.self, from: dbValue, debugInfo: debugInfo()) } } } @@ -120,12 +122,10 @@ extension DatabaseValueConvertible { extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// Performs lossless conversion from a statement value. - /// - /// - throws: ValueConversionError @inline(__always) - static func decode(from sqliteStatement: SQLiteStatement, index: Int32, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Self { + static func decode(from sqliteStatement: SQLiteStatement, index: Int32, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Self { if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { - throw ValueConversionError(dbValue: .null, debugInfo: debugInfo()) + fatalConversionError(to: Self.self, from: .null, debugInfo: debugInfo()) } return self.init(sqliteStatement: sqliteStatement, index: index) } @@ -146,41 +146,41 @@ extension Row { func decodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { - return try Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } @inline(__always) func decode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { - return try Value.decode(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } @inline(__always) func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { if let sqliteStatement = sqliteStatement { return Value.decodeIfPresent(from: sqliteStatement, index: Int32(index)) } - return try impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) } @inline(__always) func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { if let sqliteStatement = sqliteStatement { - return try Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + return Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) } - return try impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + return impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) } } diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index 356b64172f..d27adbb86c 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -70,10 +70,16 @@ public final class DatabaseValueCursor : Cursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return require { try Value.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) } + return Value.decode( + from: dbValue, + debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) - throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + throw DatabaseError( + resultCode: code, + message: statement.database.lastErrorMessage, + sql: statement.sql, + arguments: statement.arguments) } } } @@ -110,10 +116,16 @@ public final class NullableDatabaseValueCursor return nil case SQLITE_ROW: let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) - return require { try Value.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) } + return Value.decodeIfPresent( + from: dbValue, + debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) - throw DatabaseError(resultCode: code, message: statement.database.lastErrorMessage, sql: statement.sql, arguments: statement.arguments) + throw DatabaseError( + resultCode: code, + message: statement.database.lastErrorMessage, + sql: statement.sql, + arguments: statement.arguments) } } } diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 512f767f0e..4de7c25790 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -195,10 +195,10 @@ extension Row { /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try decodeIfPresent( + return decodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -215,10 +215,10 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try fastDecodeIfPresent( + return fastDecodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -230,10 +230,10 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try decode( + return decode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the value at given index, converted to the requested type. @@ -249,10 +249,10 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try fastDecode( + return fastDecode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -288,10 +288,10 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return require { try decodeIfPresent( + return decodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -310,10 +310,10 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return require { try fastDecodeIfPresent( + return fastDecodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -330,10 +330,10 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return require { try decode( + return decode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the value at given column, converted to the requested type. @@ -354,10 +354,10 @@ extension Row { // Programmer error fatalError("no such column: \(columnName)") } - return require { try fastDecode( + return fastDecode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -441,9 +441,9 @@ extension Row { /// than the row's lifetime. public func dataNoCopy(atIndex index: Int) -> Data? { GRDBPrecondition(index >= 0 && index < count, "row index out of range") - return require { try impl.dataNoCopy( + return impl.dataNoCopy( atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) } /// Returns the optional Data at given column. @@ -461,9 +461,9 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return require { try impl.dataNoCopy( + return impl.dataNoCopy( atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } + debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) } /// Returns the optional `NSData` at given column. @@ -1205,9 +1205,9 @@ protocol RowImpl { func columnName(atUncheckedIndex index: Int) -> String func hasNull(atUncheckedIndex index:Int) -> Bool func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue - func fastDecode(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value - func fastDecodeIfPresent(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? - func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? + func fastDecode(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value + func fastDecodeIfPresent(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? + func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? /// Returns the index of the leftmost column that matches *name* (case-insensitive) func index(ofColumn name: String) -> Int? @@ -1242,24 +1242,24 @@ extension RowImpl { func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { // default implementation is slow - return try Value.decode(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + return Value.decode(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { // default implementation is slow - return try Value.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + return Value.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } - func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { + func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { // default implementation copies - return try Data.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + return Data.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) } } @@ -1359,7 +1359,7 @@ private struct StatementRowImpl : RowImpl { return sqlite3_column_type(sqliteStatement, Int32(index)) == SQLITE_NULL } - func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? { + func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { guard sqlite3_column_type(sqliteStatement, Int32(index)) != SQLITE_NULL else { return nil } @@ -1376,14 +1376,14 @@ private struct StatementRowImpl : RowImpl { func fastValue( atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { - return try Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + return Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) } func fastOptionalValue( atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { return Value.decodeIfPresent(from: sqliteStatement, index: Int32(index)) } diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 0f68e839fc..6cdf94affb 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -504,27 +504,27 @@ struct AdaptedRowImpl : RowImpl { func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation + return base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Value? + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation + return base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } func dataNoCopy( atUncheckedIndex index:Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) throws -> Data? + debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return try base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation + return base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } func columnName(atUncheckedIndex index: Int) -> String { diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index abf1f34a60..70bb71814c 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -81,7 +81,10 @@ public final class ColumnCursor Bool { return require { try Bool.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Int.Type) throws -> Int { return require { try Int.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Int8.Type) throws -> Int8 { return require { try Int8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Int16.Type) throws -> Int16 { return require { try Int16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Int32.Type) throws -> Int32 { return require { try Int32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Int64.Type) throws -> Int64 { return require { try Int64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: UInt.Type) throws -> UInt { return require { try UInt.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: UInt8.Type) throws -> UInt8 { return require { try UInt8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: UInt16.Type) throws -> UInt16 { return require { try UInt16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: UInt32.Type) throws -> UInt32 { return require { try UInt32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: UInt64.Type) throws -> UInt64 { return require { try UInt64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Float.Type) throws -> Float { return require { try Float.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: Double.Type) throws -> Double { return require { try Double.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } - func decode(_ type: String.Type) throws -> String { return require { try String.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } } + func decode(_ type: Bool.Type) throws -> Bool { return Bool.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int.Type) throws -> Int { return Int.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int8.Type) throws -> Int8 { return Int8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int16.Type) throws -> Int16 { return Int16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int32.Type) throws -> Int32 { return Int32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Int64.Type) throws -> Int64 { return Int64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt.Type) throws -> UInt { return UInt.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt8.Type) throws -> UInt8 { return UInt8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt16.Type) throws -> UInt16 { return UInt16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt32.Type) throws -> UInt32 { return UInt32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: UInt64.Type) throws -> UInt64 { return UInt64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Float.Type) throws -> Float { return Float.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Double.Type) throws -> Double { return Double.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: String.Type) throws -> String { return String.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } /// Decodes a single value of the given type. /// @@ -41,7 +41,7 @@ private struct DatabaseValueDecodingContainer: SingleValueDecodingContainer { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows custom database decoding, such as decoding Date from // String, for example. - return require { try type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) as! T } + return type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) as! T } else { return try T(from: DatabaseValueDecoder(dbValue: dbValue, codingPath: codingPath)) } diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index 450aab87e7..4f6d86cfb0 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -75,7 +75,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return require { try type.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T? } + return type.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T? } else if dbValue.isNull { return nil } else { @@ -114,7 +114,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return require { try type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T } + return type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T } else { return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } @@ -227,7 +227,7 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return require { try type.decode(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(row: row, columnName: column.stringValue)) as! T } + return type.decode(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(row: row, columnName: column.stringValue)) as! T } else { return try T(from: RowDecoder(row: row, codingPath: [column])) } From 220ce379e19744c097f211c486754596c4564630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 9 Aug 2018 14:00:33 +0200 Subject: [PATCH 11/31] Test conversion error messages --- GRDB/Core/DatabasePool.swift | 2 +- GRDB/Core/DatabaseSnapshot.swift | 2 +- GRDB/Core/DatabaseValueConversion.swift | 54 +++++--- GRDB/Core/DatabaseValueConvertible.swift | 4 +- GRDB/Core/Row.swift | 20 +-- GRDB/Core/Statement.swift | 4 +- GRDB/Core/StatementColumnConvertible.swift | 2 +- GRDB/FTS/FTS5Pattern.swift | 2 +- GRDB/Record/FetchableRecord+Decodable.swift | 6 +- .../DatabasePoolSchemaCacheTests.swift | 2 +- .../DatabaseValueConversionErrorTests.swift | 128 +++++++++++++----- Tests/GRDBTests/GRDBTestCase.swift | 4 +- 12 files changed, 152 insertions(+), 78 deletions(-) diff --git a/GRDB/Core/DatabasePool.swift b/GRDB/Core/DatabasePool.swift index bee8f21e94..bea877ee30 100644 --- a/GRDB/Core/DatabasePool.swift +++ b/GRDB/Core/DatabasePool.swift @@ -427,7 +427,7 @@ extension DatabasePool : DatabaseReader { do { try db.beginTransaction(.deferred) assert(db.isInsideTransaction) - try db.makeSelectStatement("SELECT rootpage FROM sqlite_master").cursor().next() + try db.makeSelectStatement("SELECT rootpage FROM sqlite_master").makeCursor().next() } catch { readError = error semaphore.signal() // Release the writer queue and rethrow error diff --git a/GRDB/Core/DatabaseSnapshot.swift b/GRDB/Core/DatabaseSnapshot.swift index a8207b2fe6..6beb4d5ed6 100644 --- a/GRDB/Core/DatabaseSnapshot.swift +++ b/GRDB/Core/DatabaseSnapshot.swift @@ -35,7 +35,7 @@ public class DatabaseSnapshot : DatabaseReader { // Take snapshot // See DatabasePool.readFromCurrentState for a complete discussion - try db.makeSelectStatement("SELECT rootpage FROM sqlite_master").cursor().next() + try db.makeSelectStatement("SELECT rootpage FROM sqlite_master").makeCursor().next() } } diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 5ee4f7266f..7a12181417 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -19,43 +19,55 @@ struct ValueConversionDebuggingInfo { } private var source: Source? private var column: Column? - - init(statement: SelectStatement? = nil, row: Row? = nil, columnIndex: Int? = nil, columnName: String? = nil) { - fatalError("not implemented") + + init(_ source: Source? = nil, _ column: Column? = nil) { + self.source = source + self.column = column } var sql: String? { guard let source = source else { return nil } switch source { - case .statement(let statement): return statement.sql - case .row(let row): return row.statement?.sql - case .sql(let sql, _): return sql + case .statement(let statement): + return statement.sql + case .row(let row): + return row.statement?.sql + case .sql(let sql, _): + return sql } } var arguments: StatementArguments? { guard let source = source else { return nil } switch source { - case .statement(let statement): return statement.arguments - case .row(let row): return row.statement?.arguments - case .sql(_, let arguments): return arguments + case .statement(let statement): + return statement.arguments + case .row(let row): + return row.statement?.arguments + case .sql(_, let arguments): + return arguments } } var row: Row? { guard let source = source else { return nil } switch source { - case .statement(let statement): return Row(statement: statement) - case .row(let row): return row - case .sql: return nil + case .statement(let statement): + return Row(statement: statement) + case .row(let row): + return row + case .sql: + return nil } } var columnIndex: Int? { guard let column = column else { return nil } switch column { - case .columnIndex(let index): return index - case .columnName(let name): return row?.index(ofColumn: name) + case .columnIndex(let index): + return index + case .columnName(let name): + return row?.index(ofColumn: name) } } @@ -66,13 +78,14 @@ struct ValueConversionDebuggingInfo { guard let row = row else { return nil } let rowIndex = row.index(row.startIndex, offsetBy: index) return row[rowIndex].0 - case .columnName(let name): return name + case .columnName(let name): + return name } } } -/// The canonical conversion fatal error -func fatalConversionError(to: T.Type, from dbValue: DatabaseValue, debugInfo: ValueConversionDebuggingInfo) -> Never { +/// The canonical conversion error message +func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue, debugInfo: ValueConversionDebuggingInfo) -> String { var message = "could not convert database value \(dbValue) to \(T.self)" var extras: [String] = [] if let columnName = debugInfo.columnName { @@ -93,7 +106,12 @@ func fatalConversionError(to: T.Type, from dbValue: DatabaseValue, debugInfo: if extras.isEmpty == false { message += " (" + extras.joined(separator: ", ") + ")" } - fatalError(message) + return message +} + +/// The canonical conversion fatal error +func fatalConversionError(to: T.Type, from dbValue: DatabaseValue, debugInfo: ValueConversionDebuggingInfo, file: StaticString = #file, line: UInt = #line) -> Never { + fatalError(conversionErrorMessage(to: T.self, from: dbValue, debugInfo: debugInfo), file: file, line: line) } extension DatabaseValueConvertible { diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index d27adbb86c..e35df1e4d7 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -72,7 +72,7 @@ public final class DatabaseValueCursor : Cursor let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) return Value.decode( from: dbValue, - debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) + debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(Int(columnIndex)))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError( @@ -118,7 +118,7 @@ public final class NullableDatabaseValueCursor let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) return Value.decodeIfPresent( from: dbValue, - debugInfo: ValueConversionDebuggingInfo(statement: statement, columnIndex: Int(columnIndex))) + debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(Int(columnIndex)))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError( diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 4de7c25790..0914805585 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -198,7 +198,7 @@ extension Row { return decodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) } /// Returns the value at given index, converted to the requested type. @@ -218,7 +218,7 @@ extension Row { return fastDecodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) } /// Returns the value at given index, converted to the requested type. @@ -233,7 +233,7 @@ extension Row { return decode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) } /// Returns the value at given index, converted to the requested type. @@ -252,7 +252,7 @@ extension Row { return fastDecode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -291,7 +291,7 @@ extension Row { return decodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } /// Returns the value at given column, converted to the requested type. @@ -313,7 +313,7 @@ extension Row { return fastDecodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } /// Returns the value at given column, converted to the requested type. @@ -333,7 +333,7 @@ extension Row { return decode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } /// Returns the value at given column, converted to the requested type. @@ -357,7 +357,7 @@ extension Row { return fastDecode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -443,7 +443,7 @@ extension Row { GRDBPrecondition(index >= 0 && index < count, "row index out of range") return impl.dataNoCopy( atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) } /// Returns the optional Data at given column. @@ -463,7 +463,7 @@ extension Row { } return impl.dataNoCopy( atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(row: self, columnIndex: index, columnName: columnName)) + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } /// Returns the optional `NSData` at given column. diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index cb3e30cd77..cb8679e053 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -356,7 +356,7 @@ public final class SelectStatement : Statement { /// Creates a cursor over the statement. This cursor does not produce any /// value, and is only intended to give access to the sqlite3_step() /// low-level function. - func cursor(arguments: StatementArguments? = nil) -> StatementCursor { + func makeCursor(arguments: StatementArguments? = nil) -> StatementCursor { return StatementCursor(statement: self, arguments: arguments) } @@ -376,7 +376,7 @@ extension SelectStatement: AuthorizedStatement { } /// /// try dbQueue.read { db in /// let statement = db.makeSelectStatement("SELECT * FROM player") -/// let cursor: StatementCursor = statement.cursor() +/// let cursor: StatementCursor = statement.makeCursor() /// } public final class StatementCursor: Cursor { public let statement: SelectStatement diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 70bb71814c..bdf5af0f4c 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -84,7 +84,7 @@ public final class ColumnCursor: KeyedDecodingContainer 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.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T? + return type.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName(key.stringValue))) as! T? } else if dbValue.isNull { return nil } else { @@ -114,7 +114,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer 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: dbValue, debugInfo: ValueConversionDebuggingInfo(row: row, columnName: key.stringValue)) as! T + return type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName(key.stringValue))) as! T } else { return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } @@ -227,7 +227,7 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return type.decode(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(row: row, columnName: column.stringValue)) as! T + return type.decode(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName(column.stringValue))) as! T } else { return try T(from: RowDecoder(row: row, codingPath: [column])) } diff --git a/Tests/GRDBTests/DatabasePoolSchemaCacheTests.swift b/Tests/GRDBTests/DatabasePoolSchemaCacheTests.swift index fcf5f8d29a..592227a394 100644 --- a/Tests/GRDBTests/DatabasePoolSchemaCacheTests.swift +++ b/Tests/GRDBTests/DatabasePoolSchemaCacheTests.swift @@ -168,7 +168,7 @@ class DatabasePoolSchemaCacheTests : GRDBTestCase { _ = s1.wait(timeout: .distantFuture) try! dbPool.read { db in // activate snapshot isolation so that foo table is visible during the whole read. Any read is enough. - try db.makeSelectStatement("SELECT * FROM sqlite_master").cursor().next() + try db.makeSelectStatement("SELECT * FROM sqlite_master").makeCursor().next() // warm cache _ = try db.primaryKey("foo") // cache contains the primary key diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index ccb0031cc1..4f3ca1de32 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -39,41 +39,97 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { case valid } - func testError() throws { -// // TODO: find a way to turn those into real tests -// let dbQueue = try makeDatabaseQueue() -// try dbQueue.read { db in -// let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") -// statement.arguments = ["invalid"] -// let row = try Row.fetchOne(statement)! -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) -// _ = Record1(row: row) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try Record1.fetchOne(statement) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) -// _ = Record2(row: row) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try Record2.fetchOne(statement) -// -// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"]) -// _ = Record3(row: row) -// -// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try Record3.fetchOne(statement) -// -// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"]) -// _ = Record4(row: row) -// -// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try Record4.fetchOne(statement) -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try String.fetchAll(statement) -// + func testConversionErrorMessage() throws { + // Those tests are tightly coupled to GRDB decoding code. + // Each test comes with a (commented) crashing code that triggers it. + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! + + // _ = Record1(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + + // _ = try Record1.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + + // _ = Record2(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + + // _ = try Record2.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + + // _ = Record3(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + + // _ = try Record3.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + + // _ = Record4(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + + // _ = try Record4.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + + // _ = try String.fetchAll(statement) + try statement.makeCursor().forEach { + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: .null, + debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(0))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + // // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = row["name"] as String // @@ -91,6 +147,6 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { // // // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) // _ = row[0] as Value1 -// } + } } } diff --git a/Tests/GRDBTests/GRDBTestCase.swift b/Tests/GRDBTests/GRDBTestCase.swift index 314cb5b671..9989a98829 100644 --- a/Tests/GRDBTests/GRDBTestCase.swift +++ b/Tests/GRDBTests/GRDBTestCase.swift @@ -147,7 +147,7 @@ class GRDBTestCase: XCTestCase { // Compare SQL strings (ignoring leading and trailing white space and semicolons. func assertEqualSQL(_ db: Database, _ request: Request, _ sql: String, file: StaticString = #file, line: UInt = #line) throws { let (statement, _) = try request.prepare(db) - try statement.cursor().next() + try statement.makeCursor().next() assertEqualSQL(lastSQLQuery, sql, file: file, line: line) } @@ -161,7 +161,7 @@ class GRDBTestCase: XCTestCase { func sql(_ databaseReader: DatabaseReader, _ request: Request) -> String { return try! databaseReader.unsafeRead { db in let (statement, _) = try request.prepare(db) - try statement.cursor().next() + try statement.makeCursor().next() return lastSQLQuery } } From cef5d252285f8b9c2486c5366906fe70a4ab841b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Thu, 9 Aug 2018 19:19:22 +0200 Subject: [PATCH 12/31] Test conversion error messages --- .../DatabaseValueConversionErrorTests.swift | 108 +++++++++--------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index 4f3ca1de32..aeaa529197 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -41,7 +41,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { func testConversionErrorMessage() throws { // Those tests are tightly coupled to GRDB decoding code. - // Each test comes with a (commented) crashing code that triggers it. + // 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 NULL AS name, ? AS team") @@ -49,23 +49,6 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { let row = try Row.fetchOne(statement)! // _ = Record1(row: row) - XCTAssertEqual( - conversionErrorMessage( - to: String.self, - from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), - "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") - - // _ = try Record1.fetchOne(statement) - try Row.fetchCursor(statement).forEach { row in - XCTAssertEqual( - conversionErrorMessage( - to: String.self, - from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), - "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") - } - // _ = Record2(row: row) XCTAssertEqual( conversionErrorMessage( @@ -74,6 +57,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + // _ = try Record1.fetchOne(statement) // _ = try Record2.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( @@ -85,23 +69,6 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } // _ = Record3(row: row) - XCTAssertEqual( - conversionErrorMessage( - to: Value1.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") - - // _ = try Record3.fetchOne(statement) - try Row.fetchCursor(statement).forEach { row in - XCTAssertEqual( - conversionErrorMessage( - to: Value1.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") - } - // _ = Record4(row: row) XCTAssertEqual( conversionErrorMessage( @@ -110,6 +77,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + // _ = try Record3.fetchOne(statement) // _ = try Record4.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( @@ -130,23 +98,59 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) -// _ = row["name"] as String -// -// // could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) -// _ = row[0] as String -// -// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try Value1.fetchAll(statement) -// -// // could not convert database value "invalid" to Value1 (column: `team`, column index: 1, row: [name:NULL team:"invalid"], statement: `SELECT NULL AS name, ? AS team`, arguments: ["invalid"]) -// _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) -// -// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) -// _ = row["name"] as Value1 -// -// // could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:"invalid"]) -// _ = row[0] as Value1 + // _ = row["name"] as String + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + + // _ = row[0] as String + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row[0], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnIndex(0))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + + // _ = try Value1.fetchAll(statement) + try statement.makeCursor().forEach { + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: 0), + debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(0))), + "could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + + // _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) + let adapter = SuffixRowAdapter(fromIndex: 1) + let columnIndex = try adapter.baseColumnIndex(atIndex: 0, layout: statement) + try statement.makeCursor().forEach { _ in + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: Int32(columnIndex)), + debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(columnIndex))), + "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + + // _ = row["name"] as Value1 + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + + // _ = row[0] as Value1 + XCTAssertEqual( + conversionErrorMessage( + to: Value1.self, + from: row[0], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnIndex(0))), + "could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") } } } From cbda1f92116fa1d6455d806905053d1c650ac0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 10 Aug 2018 04:52:23 +0200 Subject: [PATCH 13/31] Fix DatabaseValueConvertible implementation for Decodable types that need a single-value container They used to crash on decoding errors --- .../DatabaseValueConvertible+Decodable.swift | 125 ++++++++++++++++-- ...tabaseValueConvertibleDecodableTests.swift | 22 ++- 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift b/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift index 66a82d46b5..a5e1217d3c 100644 --- a/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift +++ b/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift @@ -15,20 +15,117 @@ private struct DatabaseValueDecodingContainer: 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 Bool.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int.Type) throws -> Int { return Int.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int8.Type) throws -> Int8 { return Int8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int16.Type) throws -> Int16 { return Int16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int32.Type) throws -> Int32 { return Int32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Int64.Type) throws -> Int64 { return Int64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt.Type) throws -> UInt { return UInt.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt8.Type) throws -> UInt8 { return UInt8.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt16.Type) throws -> UInt16 { return UInt16.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt32.Type) throws -> UInt32 { return UInt32.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: UInt64.Type) throws -> UInt64 { return UInt64.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Float.Type) throws -> Float { return Float.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: Double.Type) throws -> Double { return Double.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } - func decode(_ type: String.Type) throws -> String { return String.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) } + func decode(_ type: Bool.Type) throws -> Bool { + if let result = Bool.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Int.Type) throws -> Int { + if let result = Int.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Int8.Type) throws -> Int8 { + if let result = Int8.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Int16.Type) throws -> Int16 { + if let result = Int16.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Int32.Type) throws -> Int32 { + if let result = Int32.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Int64.Type) throws -> Int64 { + if let result = Int64.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: UInt.Type) throws -> UInt { + if let result = UInt.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: UInt8.Type) throws -> UInt8 { + if let result = UInt8.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: UInt16.Type) throws -> UInt16 { + if let result = UInt16.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: UInt32.Type) throws -> UInt32 { + if let result = UInt32.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: UInt64.Type) throws -> UInt64 { + if let result = UInt64.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Float.Type) throws -> Float { + if let result = Float.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: Double.Type) throws -> Double { + if let result = Double.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } + + func decode(_ type: String.Type) throws -> String { + if let result = String.fromDatabaseValue(dbValue) { + return result + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } + } /// Decodes a single value of the given type. /// diff --git a/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift index d5de8a5ea4..804d9dede2 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift @@ -8,7 +8,7 @@ import XCTest #endif class DatabaseValueConvertibleDecodableTests: GRDBTestCase { - func testDatabaseValueConvertibleImplementationDerivedFromDecodable() { + func testDatabaseValueConvertibleImplementationDerivedFromDecodable() throws { struct Value : Decodable, DatabaseValueConvertible { let string: String @@ -24,8 +24,24 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { // static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { ... } } - let value = Value.fromDatabaseValue("foo".databaseValue)! - XCTAssertEqual(value.string, "foo") + do { + // Success from DatabaseValue + let value = Value.fromDatabaseValue("foo".databaseValue)! + XCTAssertEqual(value.string, "foo") + } + do { + // Success from database + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let value = try Value.fetchOne(db, "SELECT 'foo'")! + XCTAssertEqual(value.string, "foo") + } + } + do { + // Failure from DatabaseValue + let value = Value.fromDatabaseValue(1.databaseValue) + XCTAssertNil(value) + } } func testCustomDatabaseValueConvertible() throws { From 7d4ebce70aade70da756d0365a7db9f2a82bbc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 10 Aug 2018 04:52:59 +0200 Subject: [PATCH 14/31] TODO conversion error message for adapted rows --- GRDB/Core/RowAdapter.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 6cdf94affb..52bfb55e08 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -506,6 +506,7 @@ struct AdaptedRowImpl : RowImpl { atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { + // TODO: expose the unadapted row and index in debugInfo. Test with a scoped row. let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) return base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } @@ -515,6 +516,7 @@ struct AdaptedRowImpl : RowImpl { atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { + // TODO: expose the unadapted row and index in debugInfo. Test with a scoped row. let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) return base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } @@ -523,6 +525,7 @@ struct AdaptedRowImpl : RowImpl { atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { + // TODO: expose the unadapted row and index in debugInfo. Test with a scoped row. let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) return base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } From 9f2058f6d9390ff78c83ae37a4d550abef9e4603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 10 Aug 2018 05:01:30 +0200 Subject: [PATCH 15/31] Fix DatabaseValueConvertible implementation for Decodable types that need a single-value container --- .../DatabaseValueConvertible+Decodable.swift | 6 ++++- ...tabaseValueConvertibleDecodableTests.swift | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift b/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift index a5e1217d3c..afb22a49e4 100644 --- a/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift +++ b/GRDB/Core/Support/StandardLibrary/DatabaseValueConvertible+Decodable.swift @@ -138,7 +138,11 @@ private struct DatabaseValueDecodingContainer: SingleValueDecodingContainer { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows custom database decoding, such as decoding Date from // String, for example. - return type.decode(from: dbValue, debugInfo: ValueConversionDebuggingInfo()) as! T + if let result = type.fromDatabaseValue(dbValue) { + return result as! T + } else { + throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch") + } } else { return try T(from: DatabaseValueDecoder(dbValue: dbValue, codingPath: codingPath)) } diff --git a/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift index 804d9dede2..0276872a4f 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift @@ -24,10 +24,28 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { // static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { ... } } + struct ValueWrapper : Decodable, DatabaseValueConvertible { + let value: Value + + init(from decoder: Decoder) throws { + value = try decoder.singleValueContainer().decode(Value.self) + } + + var databaseValue: DatabaseValue { + preconditionFailure("unused") + } + + // Infered, tested + // static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { ... } + } + do { // Success from DatabaseValue let value = Value.fromDatabaseValue("foo".databaseValue)! XCTAssertEqual(value.string, "foo") + + let wrapper = ValueWrapper.fromDatabaseValue("foo".databaseValue)! + XCTAssertEqual(wrapper.value.string, "foo") } do { // Success from database @@ -35,12 +53,18 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { try dbQueue.inDatabase { db in let value = try Value.fetchOne(db, "SELECT 'foo'")! XCTAssertEqual(value.string, "foo") + + let wrapper = try ValueWrapper.fetchOne(db, "SELECT 'foo'")! + XCTAssertEqual(wrapper.value.string, "foo") } } do { // Failure from DatabaseValue let value = Value.fromDatabaseValue(1.databaseValue) XCTAssertNil(value) + + let wrapper = ValueWrapper.fromDatabaseValue(1.databaseValue) + XCTAssertNil(wrapper) } } From c480e07f08e69a19a2607a1af50f39e01206a298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 10 Aug 2018 05:21:33 +0200 Subject: [PATCH 16/31] Increase single-value containers testing --- ...tabaseValueConvertibleDecodableTests.swift | 91 ++++++++++++++++--- ...tabaseValueConvertibleEncodableTests.swift | 91 ++++++++++++++++--- 2 files changed, 156 insertions(+), 26 deletions(-) diff --git a/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift index 0276872a4f..ca05602c48 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleDecodableTests.swift @@ -8,7 +8,7 @@ import XCTest #endif class DatabaseValueConvertibleDecodableTests: GRDBTestCase { - func testDatabaseValueConvertibleImplementationDerivedFromDecodable() throws { + func testDatabaseValueConvertibleImplementationDerivedFromDecodable1() throws { struct Value : Decodable, DatabaseValueConvertible { let string: String @@ -17,13 +17,46 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { } var databaseValue: DatabaseValue { - preconditionFailure("unused") + preconditionFailure("not tested") } // Infered, tested // static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { ... } } + do { + // Success from DatabaseValue + let value = Value.fromDatabaseValue("foo".databaseValue)! + XCTAssertEqual(value.string, "foo") + } + do { + // Success from database + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let value = try Value.fetchOne(db, "SELECT 'foo'")! + XCTAssertEqual(value.string, "foo") + } + } + do { + // Failure from DatabaseValue + let value = Value.fromDatabaseValue(1.databaseValue) + XCTAssertNil(value) + } + } + + func testDatabaseValueConvertibleImplementationDerivedFromDecodable2() throws { + struct Value : Decodable, DatabaseValueConvertible { + let string: String + + init(from decoder: Decoder) throws { + string = try decoder.singleValueContainer().decode(String.self) + } + + var databaseValue: DatabaseValue { + preconditionFailure("not tested") + } + } + struct ValueWrapper : Decodable, DatabaseValueConvertible { let value: Value @@ -32,7 +65,7 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { } var databaseValue: DatabaseValue { - preconditionFailure("unused") + preconditionFailure("not tested") } // Infered, tested @@ -41,9 +74,6 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { do { // Success from DatabaseValue - let value = Value.fromDatabaseValue("foo".databaseValue)! - XCTAssertEqual(value.string, "foo") - let wrapper = ValueWrapper.fromDatabaseValue("foo".databaseValue)! XCTAssertEqual(wrapper.value.string, "foo") } @@ -51,18 +81,55 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { // Success from database let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - let value = try Value.fetchOne(db, "SELECT 'foo'")! - XCTAssertEqual(value.string, "foo") - let wrapper = try ValueWrapper.fetchOne(db, "SELECT 'foo'")! XCTAssertEqual(wrapper.value.string, "foo") } } do { // Failure from DatabaseValue - let value = Value.fromDatabaseValue(1.databaseValue) - XCTAssertNil(value) + let wrapper = ValueWrapper.fromDatabaseValue(1.databaseValue) + XCTAssertNil(wrapper) + } + } + + func testDatabaseValueConvertibleImplementationDerivedFromDecodable3() throws { + struct ValueWrapper : Decodable, DatabaseValueConvertible { + struct Nested : Decodable { + let string: String + + init(from decoder: Decoder) throws { + string = try decoder.singleValueContainer().decode(String.self) + } + } + let nested: Nested + + init(from decoder: Decoder) throws { + nested = try decoder.singleValueContainer().decode(Nested.self) + } + + var databaseValue: DatabaseValue { + preconditionFailure("not tested") + } + // Infered, tested + // static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { ... } + } + + do { + // Success from DatabaseValue + let wrapper = ValueWrapper.fromDatabaseValue("foo".databaseValue)! + XCTAssertEqual(wrapper.nested.string, "foo") + } + do { + // Success from database + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let wrapper = try ValueWrapper.fetchOne(db, "SELECT 'foo'")! + XCTAssertEqual(wrapper.nested.string, "foo") + } + } + do { + // Failure from DatabaseValue let wrapper = ValueWrapper.fromDatabaseValue(1.databaseValue) XCTAssertNil(wrapper) } @@ -73,7 +140,7 @@ class DatabaseValueConvertibleDecodableTests: GRDBTestCase { let string: String var databaseValue: DatabaseValue { - preconditionFailure("unused") + preconditionFailure("not tested") } static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { diff --git a/Tests/GRDBTests/DatabaseValueConvertibleEncodableTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleEncodableTests.swift index 7d772f5e2e..ef84e10e48 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleEncodableTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleEncodableTests.swift @@ -9,7 +9,7 @@ import Foundation #endif class DatabaseValueConvertibleEncodableTests: GRDBTestCase { - func testDatabaseValueConvertibleImplementationDerivedFromEncodable() { + func testDatabaseValueConvertibleImplementationDerivedFromEncodable1() { struct Value : Encodable, DatabaseValueConvertible { let string: String @@ -19,7 +19,7 @@ class DatabaseValueConvertibleEncodableTests: GRDBTestCase { } static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { - preconditionFailure("unused") + preconditionFailure("not tested") } // Infered, tested @@ -30,15 +30,78 @@ class DatabaseValueConvertibleEncodableTests: GRDBTestCase { XCTAssertEqual(dbValue.storage.value as! String, "foo") } - func testEncodableRawRepresentable() { - // Test that the rawValue is encoded with DatabaseValueConvertible, not with Encodable - struct Value : RawRepresentable, Encodable, DatabaseValueConvertible { - let rawValue: Date - } - - let dbValue = Value(rawValue: Date()).databaseValue - XCTAssertTrue(dbValue.storage.value is String) - } + func testDatabaseValueConvertibleImplementationDerivedFromEncodable2() { + struct Value : Encodable, DatabaseValueConvertible { + let string: String + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(string) + } + + static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { + preconditionFailure("not tested") + } + } + + struct Wrapper : Encodable, DatabaseValueConvertible { + let value: Value + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(value) + } + + static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Wrapper? { + preconditionFailure("not tested") + } + + // Infered, tested + // var databaseValue: DatabaseValue { ... } + } + + let dbValue = Wrapper(value: Value(string: "foo")).databaseValue + XCTAssertEqual(dbValue.storage.value as! String, "foo") + } + + func testDatabaseValueConvertibleImplementationDerivedFromEncodable3() { + struct Wrapper : Encodable, DatabaseValueConvertible { + struct Nested : Encodable { + let string: String + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(string) + } + } + let nested: Nested + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(nested) + } + + static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Wrapper? { + preconditionFailure("not tested") + } + + // Infered, tested + // var databaseValue: DatabaseValue { ... } + } + + let dbValue = Wrapper(nested: Wrapper.Nested(string: "foo")).databaseValue + XCTAssertEqual(dbValue.storage.value as! String, "foo") + } + + func testEncodableRawRepresentable() { + // Test that the rawValue is encoded with DatabaseValueConvertible, not with Encodable + struct Value : RawRepresentable, Encodable, DatabaseValueConvertible { + let rawValue: Date + } + + let dbValue = Value(rawValue: Date()).databaseValue + XCTAssertTrue(dbValue.storage.value is String) + } func testEncodableRawRepresentableEnum() { // Make sure this kind of declaration is possible @@ -63,7 +126,7 @@ extension DatabaseValueConvertibleEncodableTests { } static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { - preconditionFailure("unused") + preconditionFailure("not tested") } // Infered, tested @@ -92,7 +155,7 @@ extension DatabaseValueConvertibleEncodableTests { } static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { - preconditionFailure("unused") + preconditionFailure("not tested") } // Infered, tested @@ -116,7 +179,7 @@ extension DatabaseValueConvertibleEncodableTests { } static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Value? { - preconditionFailure("unused") + preconditionFailure("not tested") } // Infered, tested From d836fffd089a7d2a4a2275b4193b58f444c2f461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Fri, 10 Aug 2018 08:52:32 +0200 Subject: [PATCH 17/31] wpi: debug info for no such column errors --- GRDB/Core/DatabaseValueConversion.swift | 32 ++- GRDB/Core/Row.swift | 7 +- .../DatabaseValueConversionErrorTests.swift | 249 ++++++++++++++---- 3 files changed, 226 insertions(+), 62 deletions(-) diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 7a12181417..a5ff9c11e4 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -85,24 +85,38 @@ struct ValueConversionDebuggingInfo { } /// The canonical conversion error message -func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue, debugInfo: ValueConversionDebuggingInfo) -> String { - var message = "could not convert database value \(dbValue) to \(T.self)" +/// +/// - parameter dbValue: nil means "missing column" +func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue?, debugInfo: ValueConversionDebuggingInfo) -> String { + var message: String var extras: [String] = [] - if let columnName = debugInfo.columnName { - extras.append("column: `\(columnName)`") - } - if let columnIndex = debugInfo.columnIndex { - extras.append("column index: \(columnIndex)") + + if let dbValue = dbValue { + message = "could not convert database value \(dbValue) to \(T.self)" + if let columnName = debugInfo.columnName { + extras.append("column: `\(columnName)`") + } + if let columnIndex = debugInfo.columnIndex { + extras.append("column index: \(columnIndex)") + } + } else { + message = "missing column" + if let columnName = debugInfo.columnName { + message += " \(columnName)" + } } + if let row = debugInfo.row { extras.append("row: \(row)") } + if let sql = debugInfo.sql { extras.append("statement: `\(sql)`") if let arguments = debugInfo.arguments, arguments.isEmpty == false { extras.append("arguments: \(arguments)") } } + if extras.isEmpty == false { message += " (" + extras.joined(separator: ", ") + ")" } @@ -110,7 +124,9 @@ func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue, debugInf } /// The canonical conversion fatal error -func fatalConversionError(to: T.Type, from dbValue: DatabaseValue, debugInfo: ValueConversionDebuggingInfo, file: StaticString = #file, line: UInt = #line) -> Never { +/// +/// - parameter dbValue: nil means "missing column" +func fatalConversionError(to: T.Type, from dbValue: DatabaseValue?, debugInfo: ValueConversionDebuggingInfo, file: StaticString = #file, line: UInt = #line) -> Never { fatalError(conversionErrorMessage(to: T.self, from: dbValue, debugInfo: debugInfo), file: file, line: line) } diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 0914805585..dd8f78b59d 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -351,8 +351,11 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ columnName: String) -> Value { guard let index = index(ofColumn: columnName) else { - // Programmer error - fatalError("no such column: \(columnName)") + // No such column + fatalConversionError( + to: Value.self, + from: nil, + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } return fastDecode( Value.self, diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index aeaa529197..f8ee9d8cdd 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -7,49 +7,119 @@ import XCTest @testable import GRDB #endif +// Those tests are tightly coupled to GRDB decoding code. +// Each test comes with the (commented) crashing code snippets that trigger it. class DatabaseValueConversionErrorTests: GRDBTestCase { - struct Record1: Codable, FetchableRecord { - var name: String - var team: String - } - - struct Record2: FetchableRecord { - var name: String - var team: String + func testFetchableRecord1() throws { + struct Record: FetchableRecord { + var name: String + + init(row: Row) { + name = row["name"] + } + } - init(row: Row) { - name = row["name"] - team = row["team"] + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS name") + statement.arguments = [nil] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL], statement: `SELECT ? AS name`, arguments: [NULL])") + } + } + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS unused") + statement.arguments = ["ignored"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "missing column name (row: [unused:\"ignored\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "missing column name (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + } } } - struct Record3: Codable, FetchableRecord { - var team: Value1 - } - - struct Record4: FetchableRecord { - var team: Value1 + func testFetchableRecord2() throws { + enum Value: String, DatabaseValueConvertible, Decodable { + case valid + } - init(row: Row) { - team = row["team"] + struct Record: FetchableRecord { + var team: Value + + init(row: Row) { + team = row["team"] + } + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } } } - - enum Value1: String, DatabaseValueConvertible, Codable { - case valid - } - - func testConversionErrorMessage() throws { - // Those tests are tightly coupled to GRDB decoding code. - // Each test comes with one or several commented crashing code snippets that trigger it. + + func testDecodableFetchableRecord1() throws { + struct Record: Decodable, FetchableRecord { + var name: String + var team: String + } + let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") statement.arguments = ["invalid"] let row = try Row.fetchOne(statement)! - // _ = Record1(row: row) - // _ = Record2(row: row) + // _ = Record(row: row) XCTAssertEqual( conversionErrorMessage( to: String.self, @@ -57,8 +127,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") - // _ = try Record1.fetchOne(statement) - // _ = try Record2.fetchOne(statement) + // _ = try Record.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( conversionErrorMessage( @@ -67,26 +136,87 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } + } + } + + func testDecodableFetchableRecord2() throws { + enum Value: String, DatabaseValueConvertible, Decodable { + case valid + } + + struct Record: Decodable, FetchableRecord { + var team: Value + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! - // _ = Record3(row: row) - // _ = Record4(row: row) + // _ = Record(row: row) XCTAssertEqual( conversionErrorMessage( - to: Value1.self, + to: Value.self, from: row["team"], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") - // _ = try Record3.fetchOne(statement) - // _ = try Record4.fetchOne(statement) + // _ = try Record.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( conversionErrorMessage( - to: Value1.self, + to: Value.self, from: row["team"], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } + } + } + + func testDecodableFetchableRecord3() throws { + enum Value: String, Decodable { + case valid + } + + struct Record: Decodable, FetchableRecord { + var team: Value + } + + let dbQueue = try makeDatabaseQueue() + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["team"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + } + } + } + + func testStatementColumnConvertible() 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 NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! // _ = try String.fetchAll(statement) try statement.makeCursor().forEach { @@ -113,44 +243,59 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { from: row[0], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnIndex(0))), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + } + } + + func testDecodableDatabaseValueConvertible() throws { + enum Value: String, DatabaseValueConvertible, Decodable { + case valid + } + + // 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 NULL AS name, ? AS team") + statement.arguments = ["invalid"] + let row = try Row.fetchOne(statement)! - // _ = try Value1.fetchAll(statement) + // _ = try Value.fetchAll(statement) try statement.makeCursor().forEach { - XCTAssertEqual( + XCTAssertEqual( conversionErrorMessage( - to: Value1.self, + to: Value.self, from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: 0), debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(0))), - "could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } - // _ = try Value1.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) + // _ = try Value.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) let adapter = SuffixRowAdapter(fromIndex: 1) let columnIndex = try adapter.baseColumnIndex(atIndex: 0, layout: statement) try statement.makeCursor().forEach { _ in XCTAssertEqual( conversionErrorMessage( - to: Value1.self, + to: Value.self, from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: Int32(columnIndex)), debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(columnIndex))), - "could not convert database value \"invalid\" to Value1 (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } - // _ = row["name"] as Value1 + // _ = row["name"] as Value XCTAssertEqual( conversionErrorMessage( - to: Value1.self, + to: Value.self, from: row["name"], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), - "could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") - // _ = row[0] as Value1 + // _ = row[0] as Value XCTAssertEqual( conversionErrorMessage( - to: Value1.self, + to: Value.self, from: row[0], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnIndex(0))), - "could not convert database value NULL to Value1 (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") + "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") } } } From e46902c845bfc5ce8c1e425f1bb69bb766b0642e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 11 Aug 2018 07:16:55 +0200 Subject: [PATCH 18/31] wip: debug info for no such column errors --- GRDB/Core/DatabaseValueConversion.swift | 4 +- .../DatabaseValueConversionErrorTests.swift | 146 ++++++++++++++---- 2 files changed, 121 insertions(+), 29 deletions(-) diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index a5ff9c11e4..f474c7372f 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -100,9 +100,9 @@ func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue?, debugIn extras.append("column index: \(columnIndex)") } } else { - message = "missing column" + message = "could not read \(T.self) from missing column" if let columnName = debugInfo.columnName { - message += " \(columnName)" + message += " `\(columnName)`" } } diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index f8ee9d8cdd..d492ea8225 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -54,7 +54,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: row["name"], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), - "missing column name (row: [unused:\"ignored\"])") + "could not read String from missing column `name` (row: [unused:\"ignored\"])") // _ = try Record.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in @@ -63,7 +63,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: row["name"], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), - "missing column name (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + "could not read String from missing column `name` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -74,16 +74,16 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } struct Record: FetchableRecord { - var team: Value + var value: Value init(row: Row) { - team = row["team"] + value = row["value"] } } let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in - let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + let statement = try db.makeSelectStatement("SELECT 1, ? AS value") statement.arguments = ["invalid"] let row = try Row.fetchOne(statement)! @@ -91,18 +91,41 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { XCTAssertEqual( conversionErrorMessage( to: Value.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"])") // _ = try Record.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( conversionErrorMessage( to: Value.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"], statement: `SELECT 1, ? AS value`, arguments: [\"invalid\"])") + } + } + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS unused") + statement.arguments = ["ignored"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -137,6 +160,29 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } } + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS unused") + statement.arguments = ["ignored"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not read String from missing column `name` (row: [unused:\"ignored\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: String.self, + from: row["name"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + "could not read String from missing column `name` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + } + } } func testDecodableFetchableRecord2() throws { @@ -145,12 +191,12 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } struct Record: Decodable, FetchableRecord { - var team: Value + var value: Value } let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in - let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS value") statement.arguments = ["invalid"] let row = try Row.fetchOne(statement)! @@ -158,18 +204,41 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { XCTAssertEqual( conversionErrorMessage( to: Value.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"])") // _ = try Record.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( conversionErrorMessage( to: Value.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") + } + } + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS unused") + statement.arguments = ["ignored"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -180,12 +249,12 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } struct Record: Decodable, FetchableRecord { - var team: Value + var value: Value } let dbQueue = try makeDatabaseQueue() try dbQueue.read { db in - let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") + let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS value") statement.arguments = ["invalid"] let row = try Row.fetchOne(statement)! @@ -193,18 +262,41 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { XCTAssertEqual( conversionErrorMessage( to: Value.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"])") + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"])") // _ = try Record.fetchOne(statement) try Row.fetchCursor(statement).forEach { row in XCTAssertEqual( conversionErrorMessage( to: Value.self, - from: row["team"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("team"))), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") + } + } + try dbQueue.read { db in + let statement = try db.makeSelectStatement("SELECT ? AS unused") + statement.arguments = ["ignored"] + let row = try Row.fetchOne(statement)! + + // _ = Record(row: row) + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"])") + + // _ = try Record.fetchOne(statement) + try Row.fetchCursor(statement).forEach { row in + XCTAssertEqual( + conversionErrorMessage( + to: Value.self, + from: row["value"], + debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } From 897fd327259acb18f86de81f77286e3b0be0222a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 11 Aug 2018 12:58:04 +0200 Subject: [PATCH 19/31] Cleanup --- GRDB/Core/RowAdapter.swift | 3 --- .../DatabaseValueConversionErrorTests.swift | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 52bfb55e08..6cdf94affb 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -506,7 +506,6 @@ struct AdaptedRowImpl : RowImpl { atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value { - // TODO: expose the unadapted row and index in debugInfo. Test with a scoped row. let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) return base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } @@ -516,7 +515,6 @@ struct AdaptedRowImpl : RowImpl { atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? { - // TODO: expose the unadapted row and index in debugInfo. Test with a scoped row. let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) return base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } @@ -525,7 +523,6 @@ struct AdaptedRowImpl : RowImpl { atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { - // TODO: expose the unadapted row and index in debugInfo. Test with a scoped row. let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) return base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation } diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index d492ea8225..077249c7c5 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -20,6 +20,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } let dbQueue = try makeDatabaseQueue() + + // conversion error try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT ? AS name") statement.arguments = [nil] @@ -43,6 +45,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL], statement: `SELECT ? AS name`, arguments: [NULL])") } } + + // missing column try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT ? AS unused") statement.arguments = ["ignored"] @@ -82,6 +86,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } let dbQueue = try makeDatabaseQueue() + + // conversion error try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT 1, ? AS value") statement.arguments = ["invalid"] @@ -105,6 +111,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"], statement: `SELECT 1, ? AS value`, arguments: [\"invalid\"])") } } + + // missing column try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT ? AS unused") statement.arguments = ["ignored"] @@ -137,6 +145,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } let dbQueue = try makeDatabaseQueue() + + // conversion error try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS team") statement.arguments = ["invalid"] @@ -160,6 +170,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } } + + // missing column try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT ? AS unused") statement.arguments = ["ignored"] @@ -195,6 +207,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } let dbQueue = try makeDatabaseQueue() + + // conversion error try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS value") statement.arguments = ["invalid"] @@ -218,6 +232,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") } } + + // missing column try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT ? AS unused") statement.arguments = ["ignored"] @@ -253,6 +269,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { } let dbQueue = try makeDatabaseQueue() + + // conversion error try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT NULL AS name, ? AS value") statement.arguments = ["invalid"] @@ -276,6 +294,8 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") } } + + // missing column try dbQueue.read { db in let statement = try db.makeSelectStatement("SELECT ? AS unused") statement.arguments = ["ignored"] From 1dcb42c540626258afcd110a794c34236ab831b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 11 Aug 2018 13:03:11 +0200 Subject: [PATCH 20/31] wip: debug info for no such column errors --- GRDB/Core/Row.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index dd8f78b59d..9a51a62a5d 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -327,8 +327,11 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ columnName: String) -> Value { guard let index = index(ofColumn: columnName) else { - // Programmer error - fatalError("no such column: \(columnName)") + // No such column + fatalConversionError( + to: Value.self, + from: nil, + debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) } return decode( Value.self, From ef10e2e8edb687346662d2392f3f351fa69d25d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 11 Aug 2018 19:45:58 +0200 Subject: [PATCH 21/31] General cleanup: - rename ValueConversionDebuggingInfo to ValueConversionContext - allow missing value conversion context (when decoding a raw DatabaseValue) - rename ColumnCursor to FastDatabaseValueCursor - rename NullableColumnCursor to FastNullableDatabaseValueCursor --- CHANGELOG.md | 9 + GRDB/Core/DatabaseValue.swift | 4 +- GRDB/Core/DatabaseValueConversion.swift | 179 +++++++++--------- GRDB/Core/DatabaseValueConvertible.swift | 4 +- GRDB/Core/Row.swift | 98 ++++++---- GRDB/Core/RowAdapter.swift | 15 +- GRDB/Core/StatementColumnConvertible.swift | 38 ++-- GRDB/Record/FetchableRecord+Decodable.swift | 6 +- GRDB/Utils/Utils.swift | 1 + .../DatabasePoolReleaseMemoryTests.swift | 2 +- .../DatabaseQueueReleaseMemoryTests.swift | 2 +- .../DatabaseValueConversionErrorTests.swift | 54 +++--- ...StatementColumnConvertibleFetchTests.swift | 10 +- 13 files changed, 225 insertions(+), 197 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a23b52901..54750e3954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ Release Notes + @available(*, deprecated) func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? } + ++final class FastDatabaseValueCursor : Cursor { } ++@available(*, deprecated, renamed: "FastDatabaseValueCursor") ++typealias ColumnCursor = FastDatabaseValueCursor + ++final class FastNullableDatabaseValueCursor : Cursor { } ++@available(*, deprecated, renamed: "FastNullableDatabaseValueCursor") ++typealias NullableColumnCursor = FastNullableDatabaseValueCursor + ``` ### Documentation Diff diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 6e724ea0c5..8b682e5230 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -227,7 +227,7 @@ extension DatabaseValue { /// conversion error @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible { - return T.decode(from: self, debugInfo: ValueConversionDebuggingInfo()) + return T.decode(from: self, conversionContext: nil) } /// Converts the database value to the type Optional. @@ -251,7 +251,7 @@ extension DatabaseValue { /// conversion error @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible { - return T.decodeIfPresent(from: self, debugInfo: ValueConversionDebuggingInfo()) + return T.decodeIfPresent(from: self, conversionContext: nil) } } diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index f474c7372f..ebc9b63349 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -4,61 +4,29 @@ import SQLite3 #endif -// All primitive value conversion methods. +// MARK: - Conversion Context and Errors /// A type that helps the user understanding value conversion errors -struct ValueConversionDebuggingInfo { - enum Source { - case statement(SelectStatement) - case sql(String, StatementArguments) - case row(Row) - } - enum Column { +struct ValueConversionContext { + private enum Column { case columnIndex(Int) case columnName(String) } - private var source: Source? + var row: Row? + var sql: String? + var arguments: StatementArguments? private var column: Column? - init(_ source: Source? = nil, _ column: Column? = nil) { - self.source = source - self.column = column + func atColumn(_ columnIndex: Int) -> ValueConversionContext { + var result = self + result.column = .columnIndex(columnIndex) + return result } - var sql: String? { - guard let source = source else { return nil } - switch source { - case .statement(let statement): - return statement.sql - case .row(let row): - return row.statement?.sql - case .sql(let sql, _): - return sql - } - } - - var arguments: StatementArguments? { - guard let source = source else { return nil } - switch source { - case .statement(let statement): - return statement.arguments - case .row(let row): - return row.statement?.arguments - case .sql(_, let arguments): - return arguments - } - } - - var row: Row? { - guard let source = source else { return nil } - switch source { - case .statement(let statement): - return Row(statement: statement) - case .row(let row): - return row - case .sql: - return nil - } + func atColumn(_ columnName: String) -> ValueConversionContext { + var result = self + result.column = .columnName(columnName) + return result } var columnIndex: Int? { @@ -84,35 +52,62 @@ struct ValueConversionDebuggingInfo { } } +extension ValueConversionContext { + init(_ statement: SelectStatement) { + self.init( + row: Row(statement: statement).copy(), + sql: statement.sql, + arguments: statement.arguments, + column: nil) + } + + init(_ row: Row) { + if let statement = row.statement { + self.init( + row: row.copy(), + sql: statement.sql, + arguments: statement.arguments, + column: nil) + } else { + self.init( + row: row.copy(), + sql: nil, + arguments: nil, + column: nil) + } + } + +} + /// The canonical conversion error message /// /// - parameter dbValue: nil means "missing column" -func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue?, debugInfo: ValueConversionDebuggingInfo) -> String { +func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue?, conversionContext: ValueConversionContext?) -> String { var message: String var extras: [String] = [] if let dbValue = dbValue { message = "could not convert database value \(dbValue) to \(T.self)" - if let columnName = debugInfo.columnName { + if let columnName = conversionContext?.columnName { extras.append("column: `\(columnName)`") } - if let columnIndex = debugInfo.columnIndex { + if let columnIndex = conversionContext?.columnIndex { extras.append("column index: \(columnIndex)") } } else { message = "could not read \(T.self) from missing column" - if let columnName = debugInfo.columnName { + if let columnName = conversionContext?.columnName { message += " `\(columnName)`" } } - if let row = debugInfo.row { + if let row = conversionContext?.row { extras.append("row: \(row)") } - if let sql = debugInfo.sql { + if let sql = conversionContext?.sql { extras.append("statement: `\(sql)`") - if let arguments = debugInfo.arguments, arguments.isEmpty == false { + if let arguments = conversionContext?.arguments, arguments.isEmpty == false { extras.append("arguments: \(arguments)") } } @@ -125,48 +120,73 @@ func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue?, debugIn /// The canonical conversion fatal error /// -/// - parameter dbValue: nil means "missing column" -func fatalConversionError(to: T.Type, from dbValue: DatabaseValue?, debugInfo: ValueConversionDebuggingInfo, file: StaticString = #file, line: UInt = #line) -> Never { - fatalError(conversionErrorMessage(to: T.self, from: dbValue, debugInfo: debugInfo), file: file, line: line) +/// - parameter dbValue: nil means "missing column", for consistency with (row["missing"] as DatabaseValue? == nil) +func fatalConversionError(to: T.Type, from dbValue: DatabaseValue?, conversionContext: ValueConversionContext?, file: StaticString = #file, line: UInt = #line) -> Never { + fatalError(conversionErrorMessage(to: T.self, from: dbValue, conversionContext: conversionContext), file: file, line: line) } +// MARK: - DatabaseValueConvertible + extension DatabaseValueConvertible { /// Performs lossless conversion from a database value. - static func decode(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Self { + @inline(__always) + static func decode(from dbValue: DatabaseValue, conversionContext: @autoclosure () -> ValueConversionContext?) -> Self { if let value = fromDatabaseValue(dbValue) { return value } else { - fatalConversionError(to: Self.self, from: dbValue, debugInfo: debugInfo()) + fatalConversionError(to: Self.self, from: dbValue, conversionContext: conversionContext()) } } /// Performs lossless conversion from a database value. - static func decodeIfPresent(from dbValue: DatabaseValue, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Self? { + @inline(__always) + static func decodeIfPresent(from dbValue: DatabaseValue, conversionContext: @autoclosure () -> ValueConversionContext?) -> Self? { // Use fromDatabaseValue before checking for null: this allows DatabaseValue to convert NULL to .null. if let value = fromDatabaseValue(dbValue) { return value } else if dbValue.isNull { return nil } else { - fatalConversionError(to: Self.self, from: dbValue, debugInfo: debugInfo()) + fatalConversionError(to: Self.self, from: dbValue, conversionContext: conversionContext()) } } } -extension DatabaseValueConvertible where Self: StatementColumnConvertible { +extension Row { + @inline(__always) + func decodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + { + return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + } + @inline(__always) + func decode( + _ type: Value.Type, + atUncheckedIndex index: Int, + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + { + return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + } +} + +// MARK: - DatabaseValueConvertible & StatementColumnConvertible + +extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// Performs lossless conversion from a statement value. @inline(__always) - static func decode(from sqliteStatement: SQLiteStatement, index: Int32, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Self { + static func fastDecode(from sqliteStatement: SQLiteStatement, index: Int32, conversionContext: @autoclosure () -> ValueConversionContext?) -> Self { if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { - fatalConversionError(to: Self.self, from: .null, debugInfo: debugInfo()) + fatalConversionError(to: Self.self, from: .null, conversionContext: conversionContext()) } return self.init(sqliteStatement: sqliteStatement, index: index) } /// Performs lossless conversion from a statement value. @inline(__always) - static func decodeIfPresent(from sqliteStatement: SQLiteStatement, index: Int32) -> Self? { + static func fastDecodeIfPresent(from sqliteStatement: SQLiteStatement, index: Int32) -> Self? { if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL { return nil } @@ -175,46 +195,27 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { } extension Row { - - @inline(__always) - func decodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? - { - return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) - } - - @inline(__always) - func decode( - _ type: Value.Type, - atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value - { - return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) - } - @inline(__always) func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? { if let sqliteStatement = sqliteStatement { - return Value.decodeIfPresent(from: sqliteStatement, index: Int32(index)) + return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) } - return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) } @inline(__always) func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value { if let sqliteStatement = sqliteStatement { - return Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) } - return impl.fastDecode(Value.self, atUncheckedIndex: index, debugInfo: debugInfo) + return impl.fastDecode(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) } } diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index e35df1e4d7..e1ab8a5545 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -72,7 +72,7 @@ public final class DatabaseValueCursor : Cursor let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) return Value.decode( from: dbValue, - debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(Int(columnIndex)))) + conversionContext: ValueConversionContext(statement).atColumn(Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError( @@ -118,7 +118,7 @@ public final class NullableDatabaseValueCursor let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: columnIndex) return Value.decodeIfPresent( from: dbValue, - debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(Int(columnIndex)))) + conversionContext: ValueConversionContext(statement).atColumn(Int(columnIndex))) case let code: statement.database.selectStatementDidFail(statement) throw DatabaseError( diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index 9a51a62a5d..e2f740b81b 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -171,7 +171,7 @@ extension Row { /// in performance-critical code because it can avoid decoding database /// values. public func hasNull(atIndex index: Int) -> Bool { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return impl.hasNull(atUncheckedIndex: index) } @@ -181,7 +181,7 @@ extension Row { /// Indexes span from 0 for the leftmost column to (row.count - 1) for the /// righmost column. public subscript(_ index: Int) -> DatabaseValueConvertible? { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return impl.databaseValue(atUncheckedIndex: index).storage.value } @@ -194,11 +194,11 @@ extension Row { /// value is converted to the requested type `Value`. Should this conversion /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return decodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) + conversionContext: ValueConversionContext(self).atColumn(index)) } /// Returns the value at given index, converted to the requested type. @@ -214,11 +214,11 @@ extension Row { /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value? { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return fastDecodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) + conversionContext: ValueConversionContext(self).atColumn(index)) } /// Returns the value at given index, converted to the requested type. @@ -229,11 +229,11 @@ extension Row { /// This method crashes if the fetched SQLite value is NULL, or if the /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return decode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) + conversionContext: ValueConversionContext(self).atColumn(index)) } /// Returns the value at given index, converted to the requested type. @@ -248,11 +248,11 @@ extension Row { /// StatementColumnConvertible. It *may* trigger SQLite built-in conversions /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return fastDecode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) + conversionContext: ValueConversionContext(self).atColumn(index)) } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -291,7 +291,7 @@ extension Row { return decodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } /// Returns the value at given column, converted to the requested type. @@ -313,7 +313,7 @@ extension Row { return fastDecodeIfPresent( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } /// Returns the value at given column, converted to the requested type. @@ -331,12 +331,12 @@ extension Row { fatalConversionError( to: Value.self, from: nil, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } return decode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } /// Returns the value at given column, converted to the requested type. @@ -358,12 +358,12 @@ extension Row { fatalConversionError( to: Value.self, from: nil, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } return fastDecode( Value.self, atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -446,10 +446,10 @@ extension Row { /// The returned data does not owns its bytes: it must not be used longer /// than the row's lifetime. public func dataNoCopy(atIndex index: Int) -> Data? { - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(index) return impl.dataNoCopy( atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnIndex(index))) + conversionContext: ValueConversionContext(self).atColumn(index)) } /// Returns the optional Data at given column. @@ -469,7 +469,7 @@ extension Row { } return impl.dataNoCopy( atUncheckedIndex: index, - debugInfo: ValueConversionDebuggingInfo(.row(self), .columnName(columnName))) + conversionContext: ValueConversionContext(self).atColumn(columnName)) } /// Returns the optional `NSData` at given column. @@ -486,6 +486,12 @@ extension Row { public func dataNoCopy(_ column: ColumnExpression) -> Data? { return dataNoCopy(named: column.name) } + + @inline(__always) + private func checkedIndex(_ index: Int, file: StaticString = #file, line: UInt = #line) -> Int { + GRDBPrecondition(index >= 0 && index < count, "row index out of range", file: file, line: line) + return index + } } extension Row { @@ -940,8 +946,7 @@ extension Row { /// Accesses the (ColumnName, DatabaseValue) pair at given index. public subscript(position: RowIndex) -> (String, DatabaseValue) { - let index = position.index - GRDBPrecondition(index >= 0 && index < count, "row index out of range") + let index = checkedIndex(position.index) return ( impl.columnName(atUncheckedIndex: index), impl.databaseValue(atUncheckedIndex: index)) @@ -1211,9 +1216,9 @@ protocol RowImpl { func columnName(atUncheckedIndex index: Int) -> String func hasNull(atUncheckedIndex index:Int) -> Bool func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue - func fastDecode(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value - func fastDecodeIfPresent(_ type: Value.Type, atUncheckedIndex index: Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? - func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? + func fastDecode(_ type: Value.Type, atUncheckedIndex index: Int, conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + func fastDecodeIfPresent(_ type: Value.Type, atUncheckedIndex index: Int, conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + func dataNoCopy(atUncheckedIndex index:Int, conversionContext: @autoclosure () -> ValueConversionContext?) -> Data? /// Returns the index of the leftmost column that matches *name* (case-insensitive) func index(ofColumn name: String) -> Int? @@ -1226,49 +1231,55 @@ protocol RowImpl { extension RowImpl { func copiedRow(_ row: Row) -> Row { + // unless customized, assume immutable row (see StatementRowImpl and AdaptedRowImpl for customization) return row } func unscopedRow(_ row: Row) -> Row { + // unless customized, assume unadapted row (see AdaptedRowImpl for customization) return row } func unadaptedRow(_ row: Row) -> Row { + // unless customized, assume unadapted row (see AdaptedRowImpl for customization) return row } var scopes: Row.ScopesView { + // unless customized, assume unuscoped row (see AdaptedRowImpl for customization) return Row.ScopesView() } func hasNull(atUncheckedIndex index:Int) -> Bool { + // unless customized, use slow check (see StatementRowImpl and AdaptedRowImpl for customization) return databaseValue(atUncheckedIndex: index).isNull } func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value { - // default implementation is slow - return Value.decode(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + // unless customized, use slow decoding (see StatementRowImpl and AdaptedRowImpl for customization) + return Value.decode(from: databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) } func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? { - // default implementation is slow - return Value.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + // unless customized, use slow decoding (see StatementRowImpl and AdaptedRowImpl for customization) + return Value.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) } - func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { - // default implementation copies - return Data.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), debugInfo: debugInfo) + func dataNoCopy(atUncheckedIndex index:Int, conversionContext: @autoclosure () -> ValueConversionContext?) -> Data? { + // unless customized, copy data (see StatementRowImpl and AdaptedRowImpl for customization) + return Data.decodeIfPresent(from: databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) } } +// TODO: merge with StatementCopyRowImpl eventually? /// See Row.init(dictionary:) private struct ArrayRowImpl : RowImpl { let columns: [(String, DatabaseValue)] @@ -1300,6 +1311,7 @@ private struct ArrayRowImpl : RowImpl { } +// TODO: merge with ArrayRowImpl eventually? /// See Row.init(copiedFromStatementRef:sqliteStatement:) private struct StatementCopyRowImpl : RowImpl { let dbValues: ContiguousArray @@ -1343,7 +1355,7 @@ private struct StatementRowImpl : RowImpl { init(sqliteStatement: SQLiteStatement, statementRef: Unmanaged) { self.statementRef = statementRef self.sqliteStatement = sqliteStatement - // Optimize row["..."] + // Optimize row[columnName] let lowercaseColumnNames = (0.. Bool { - // Avoid extracting values, because this modifies the statement. + // Avoid extracting values, because this modifies the SQLite statement. return sqlite3_column_type(sqliteStatement, Int32(index)) == SQLITE_NULL } - func dataNoCopy(atUncheckedIndex index:Int, debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? { + func dataNoCopy(atUncheckedIndex index:Int, conversionContext: @autoclosure () -> ValueConversionContext?) -> Data? { guard sqlite3_column_type(sqliteStatement, Int32(index)) != SQLITE_NULL else { return nil } @@ -1380,18 +1392,20 @@ private struct StatementRowImpl : RowImpl { return DatabaseValue(sqliteStatement: sqliteStatement, index: Int32(index)) } - func fastValue( + func fastDecode( + _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value { - return Value.decode(from: sqliteStatement, index: Int32(index), debugInfo: debugInfo) + return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) } - func fastOptionalValue( + func fastDecodeIfPresent( + _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? { - return Value.decodeIfPresent(from: sqliteStatement, index: Int32(index)) + return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) } func columnName(atUncheckedIndex index: Int) -> String { diff --git a/GRDB/Core/RowAdapter.swift b/GRDB/Core/RowAdapter.swift index 6cdf94affb..25c2ddc198 100644 --- a/GRDB/Core/RowAdapter.swift +++ b/GRDB/Core/RowAdapter.swift @@ -504,27 +504,24 @@ struct AdaptedRowImpl : RowImpl { func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation + return base.impl.fastDecode(Value.self, atUncheckedIndex: mappedIndex, conversionContext: conversionContext) // base.impl: Demeter violation } func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Value? + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation + return base.impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: mappedIndex, conversionContext: conversionContext) // base.impl: Demeter violation } - func dataNoCopy( - atUncheckedIndex index:Int, - debugInfo: @autoclosure () -> ValueConversionDebuggingInfo) -> Data? - { + func dataNoCopy(atUncheckedIndex index:Int, conversionContext: @autoclosure () -> ValueConversionContext?) -> Data? { let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index) - return base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, debugInfo: debugInfo) // base.impl: Demeter violation + return base.impl.dataNoCopy(atUncheckedIndex: mappedIndex, conversionContext: conversionContext) // base.impl: Demeter violation } func columnName(atUncheckedIndex index: Int) -> String { diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index bdf5af0f4c..2ec7b052b8 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -59,7 +59,7 @@ public protocol StatementColumnConvertible { /// print(name) /// } /// } -public final class ColumnCursor : Cursor { +public final class FastDatabaseValueCursor : Cursor { private let statement: SelectStatement private let sqliteStatement: SQLiteStatement private let columnIndex: Int32 @@ -81,10 +81,10 @@ public final class ColumnCursor = FastDatabaseValueCursor + /// A cursor of optional database values extracted from a single column. /// For example: /// @@ -101,7 +104,7 @@ public final class ColumnCursor") /// } /// } -public final class NullableColumnCursor : Cursor { +public final class FastNullableDatabaseValueCursor : Cursor { private let statement: SelectStatement private let sqliteStatement: SQLiteStatement private let columnIndex: Int32 @@ -123,7 +126,7 @@ public final class NullableColumnCursor = FastNullableDatabaseValueCursor + /// Types that adopt both DatabaseValueConvertible and /// StatementColumnConvertible can be efficiently initialized from /// database values. @@ -160,8 +166,8 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - adapter: Optional RowAdapter /// - returns: A cursor over fetched values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> ColumnCursor { - return try ColumnCursor(statement: statement, arguments: arguments, adapter: adapter) + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastDatabaseValueCursor { + return try FastDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter) } /// Returns an array of values fetched from a prepared statement. @@ -192,7 +198,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. public static func fetchOne(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> Self? { // fetchOne returns nil if there is no row, or if there is a row with a null value - let cursor = try NullableColumnCursor(statement: statement, arguments: arguments, adapter: adapter) + let cursor = try FastNullableDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter) return try cursor.next() ?? nil } } @@ -220,7 +226,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - adapter: Optional RowAdapter /// - returns: A cursor over fetched values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> ColumnCursor { + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastDatabaseValueCursor { return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) } @@ -277,7 +283,7 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { /// - request: A FetchRequest. /// - returns: A cursor over fetched values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, _ request: R) throws -> ColumnCursor { + public static func fetchCursor(_ db: Database, _ request: R) throws -> FastDatabaseValueCursor { let (statement, adapter) = try request.prepare(db) return try fetchCursor(statement, adapter: adapter) } @@ -333,7 +339,7 @@ extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementCol /// - parameter db: A database connection. /// - returns: A cursor over fetched values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public func fetchCursor(_ db: Database) throws -> ColumnCursor { + public func fetchCursor(_ db: Database) throws -> FastDatabaseValueCursor { return try RowDecoder.fetchCursor(db, self) } @@ -399,8 +405,8 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv /// - adapter: Optional RowAdapter /// - returns: A cursor over fetched optional values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableColumnCursor { - return try NullableColumnCursor(statement: statement, arguments: arguments, adapter: adapter) + public static func fetchCursor(_ statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastNullableDatabaseValueCursor { + return try FastNullableDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter) } /// Returns an array of optional values fetched from a prepared statement. @@ -442,7 +448,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv /// - adapter: Optional RowAdapter /// - returns: A cursor over fetched optional values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> NullableColumnCursor { + public static func fetchCursor(_ db: Database, _ sql: String, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws -> FastNullableDatabaseValueCursor { return try fetchCursor(db, SQLRequest(sql, arguments: arguments, adapter: adapter)) } @@ -484,7 +490,7 @@ extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConv /// - request: A FetchRequest. /// - returns: A cursor over fetched optional values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public static func fetchCursor(_ db: Database, _ request: R) throws -> NullableColumnCursor { + public static func fetchCursor(_ db: Database, _ request: R) throws -> FastNullableDatabaseValueCursor { let (statement, adapter) = try request.prepare(db) return try fetchCursor(statement, adapter: adapter) } @@ -525,7 +531,7 @@ extension FetchRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped: /// - parameter db: A database connection. /// - returns: A cursor over fetched values. /// - throws: A DatabaseError is thrown whenever an SQLite error occurs. - public func fetchCursor(_ db: Database) throws -> NullableColumnCursor { + public func fetchCursor(_ db: Database) throws -> FastNullableDatabaseValueCursor { return try Optional.fetchCursor(db, self) } diff --git a/GRDB/Record/FetchableRecord+Decodable.swift b/GRDB/Record/FetchableRecord+Decodable.swift index a535053cb3..4394165c3c 100644 --- a/GRDB/Record/FetchableRecord+Decodable.swift +++ b/GRDB/Record/FetchableRecord+Decodable.swift @@ -75,7 +75,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer 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.decodeIfPresent(from: dbValue, debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName(key.stringValue))) as! T? + return type.decodeIfPresent(from: dbValue, conversionContext: ValueConversionContext(row).atColumn(key.stringValue)) as! T? } else if dbValue.isNull { return nil } else { @@ -114,7 +114,7 @@ private struct RowKeyedDecodingContainer: KeyedDecodingContainer 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: dbValue, debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName(key.stringValue))) as! T + return type.decode(from: dbValue, conversionContext: ValueConversionContext(row).atColumn(key.stringValue)) as! T } else { return try T(from: RowDecoder(row: row, codingPath: codingPath + [key])) } @@ -227,7 +227,7 @@ private struct RowSingleValueDecodingContainer: SingleValueDecodingContainer { if let type = T.self as? DatabaseValueConvertible.Type { // Prefer DatabaseValueConvertible decoding over Decodable. // This allows decoding Date from String, or DatabaseValue from NULL. - return type.decode(from: row[column.stringValue], debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName(column.stringValue))) as! T + return type.decode(from: row[column.stringValue], conversionContext: ValueConversionContext(row).atColumn(column.stringValue)) as! T } else { return try T(from: RowDecoder(row: row, codingPath: [column])) } diff --git a/GRDB/Utils/Utils.swift b/GRDB/Utils/Utils.swift index 8766e852cd..08d085da87 100644 --- a/GRDB/Utils/Utils.swift +++ b/GRDB/Utils/Utils.swift @@ -46,6 +46,7 @@ extension Optional : _OptionalProtocol { // MARK: - Internal /// Reserved for GRDB: do not use. +@inline(__always) func GRDBPrecondition(_ condition: @autoclosure() -> Bool, _ message: @autoclosure() -> String = "", file: StaticString = #file, line: UInt = #line) { /// Custom precondition function which aims at solving /// https://bugs.swift.org/browse/SR-905 and diff --git a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift index c9ada40daa..963269a3d3 100644 --- a/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabasePoolReleaseMemoryTests.swift @@ -205,7 +205,7 @@ class DatabasePoolReleaseMemoryTests: GRDBTestCase { } } - var cursor: ColumnCursor? = nil + var cursor: FastDatabaseValueCursor? = nil do { try! makeDatabasePool().write { db in try db.execute("CREATE TABLE items (id INTEGER PRIMARY KEY)") diff --git a/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift b/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift index 70b8066f5e..7f24263509 100644 --- a/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift +++ b/Tests/GRDBTests/DatabaseQueueReleaseMemoryTests.swift @@ -120,7 +120,7 @@ class DatabaseQueueReleaseMemoryTests: GRDBTestCase { } } - var cursor: ColumnCursor? = nil + var cursor: FastDatabaseValueCursor? = nil do { try! makeDatabaseQueue().inDatabase { db in try db.execute("CREATE TABLE items (id INTEGER PRIMARY KEY)") diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index 077249c7c5..8a7d91a0b5 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -32,7 +32,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL])") // _ = try Record.fetchOne(statement) @@ -41,7 +41,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL], statement: `SELECT ? AS name`, arguments: [NULL])") } } @@ -57,7 +57,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not read String from missing column `name` (row: [unused:\"ignored\"])") // _ = try Record.fetchOne(statement) @@ -66,7 +66,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not read String from missing column `name` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } @@ -98,7 +98,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"])") // _ = try Record.fetchOne(statement) @@ -107,7 +107,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"], statement: `SELECT 1, ? AS value`, arguments: [\"invalid\"])") } } @@ -123,7 +123,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"])") // _ = try Record.fetchOne(statement) @@ -132,7 +132,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } @@ -157,7 +157,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") // _ = try Record.fetchOne(statement) @@ -166,7 +166,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } } @@ -182,7 +182,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not read String from missing column `name` (row: [unused:\"ignored\"])") // _ = try Record.fetchOne(statement) @@ -191,7 +191,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not read String from missing column `name` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } @@ -219,7 +219,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"])") // _ = try Record.fetchOne(statement) @@ -228,7 +228,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") } } @@ -244,7 +244,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"])") // _ = try Record.fetchOne(statement) @@ -253,7 +253,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } @@ -281,7 +281,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"])") // _ = try Record.fetchOne(statement) @@ -290,7 +290,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") } } @@ -306,7 +306,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"])") // _ = try Record.fetchOne(statement) @@ -315,7 +315,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["value"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("value"))), + conversionContext: ValueConversionContext(row).atColumn("value")), "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } @@ -336,7 +336,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: .null, - debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(0))), + conversionContext: ValueConversionContext(statement).atColumn(0)), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } @@ -345,7 +345,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") // _ = row[0] as String @@ -353,7 +353,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: String.self, from: row[0], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnIndex(0))), + conversionContext: ValueConversionContext(row).atColumn(0)), "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") } } @@ -377,7 +377,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: 0), - debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(0))), + conversionContext: ValueConversionContext(statement).atColumn(0)), "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } @@ -389,7 +389,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: Int32(columnIndex)), - debugInfo: ValueConversionDebuggingInfo(.statement(statement), .columnIndex(columnIndex))), + conversionContext: ValueConversionContext(statement).atColumn(columnIndex)), "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } @@ -398,7 +398,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row["name"], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnName("name"))), + conversionContext: ValueConversionContext(row).atColumn("name")), "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") // _ = row[0] as Value @@ -406,7 +406,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { conversionErrorMessage( to: Value.self, from: row[0], - debugInfo: ValueConversionDebuggingInfo(.row(row), .columnIndex(0))), + conversionContext: ValueConversionContext(row).atColumn(0)), "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"])") } } diff --git a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift index 97121e4299..7a4f80ff06 100644 --- a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift +++ b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift @@ -86,7 +86,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { func testFetchCursor() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - func test(_ cursor: ColumnCursor) throws { + func test(_ cursor: FastDatabaseValueCursor) throws { var i = try cursor.next()! XCTAssertEqual(i.int, 1) XCTAssertTrue(i.fast) @@ -120,7 +120,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { let customError = NSError(domain: "Custom", code: 0xDEAD) dbQueue.add(function: DatabaseFunction("throw", argumentCount: 0, pure: true) { _ in throw customError }) try dbQueue.inDatabase { db in - func test(_ cursor: ColumnCursor, sql: String) throws { + func test(_ cursor: FastDatabaseValueCursor, sql: String) throws { do { _ = try cursor.next() XCTFail() @@ -161,7 +161,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { func testFetchCursorCompilationFailure() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - func test(_ cursor: @autoclosure () throws -> ColumnCursor, sql: String) throws { + func test(_ cursor: @autoclosure () throws -> FastDatabaseValueCursor, sql: String) throws { do { _ = try cursor() XCTFail() @@ -426,7 +426,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { func testOptionalFetchCursor() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - func test(_ cursor: NullableColumnCursor) throws { + func test(_ cursor: FastNullableDatabaseValueCursor) throws { let i = try cursor.next()! XCTAssertEqual(i!.int, 1) XCTAssertTrue(i!.fast) @@ -456,7 +456,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { func testOptionalFetchCursorCompilationFailure() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in - func test(_ cursor: @autoclosure () throws -> NullableColumnCursor, sql: String) throws { + func test(_ cursor: @autoclosure () throws -> FastNullableDatabaseValueCursor, sql: String) throws { do { _ = try cursor() XCTFail() From bf5420de261ecd38920f26bbd7db943f0fef3d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 10:20:10 +0200 Subject: [PATCH 22/31] More cleanup --- GRDB/Core/DatabaseValueConversion.swift | 29 ++++++++++++------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index ebc9b63349..e8a6efb25d 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -76,7 +76,6 @@ extension ValueConversionContext { column: nil) } } - } /// The canonical conversion error message @@ -154,21 +153,21 @@ extension DatabaseValueConvertible { extension Row { @inline(__always) - func decodeIfPresent( + func decode( _ type: Value.Type, atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value { - return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) } @inline(__always) - func decode( + func decodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? { - return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) } } @@ -196,26 +195,26 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { extension Row { @inline(__always) - func fastDecodeIfPresent( + func fastDecode( _ type: Value.Type, atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value { if let sqliteStatement = sqliteStatement { - return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) + return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) } - return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) + return impl.fastDecode(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) } @inline(__always) - func fastDecode( + func fastDecodeIfPresent( _ type: Value.Type, atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? { if let sqliteStatement = sqliteStatement { - return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) + return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) } - return impl.fastDecode(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) + return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) } } From 80fe7f87b18524c0f0933ddf46865143f6e9c926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 11:39:48 +0200 Subject: [PATCH 23/31] More cleanup --- GRDB/Core/DatabaseValueConversion.swift | 46 ------------------------- GRDB/Core/Row.swift | 46 ++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 47 deletions(-) diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index e8a6efb25d..15ca1093fb 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -151,26 +151,6 @@ extension DatabaseValueConvertible { } } -extension Row { - @inline(__always) - func decode( - _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value - { - return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) - } - - @inline(__always) - func decodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? - { - return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) - } -} - // MARK: - DatabaseValueConvertible & StatementColumnConvertible extension DatabaseValueConvertible where Self: StatementColumnConvertible { @@ -192,29 +172,3 @@ extension DatabaseValueConvertible where Self: StatementColumnConvertible { return self.init(sqliteStatement: sqliteStatement, index: index) } } - -extension Row { - @inline(__always) - func fastDecode( - _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value - { - if let sqliteStatement = sqliteStatement { - return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) - } - return impl.fastDecode(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) - } - - @inline(__always) - func fastDecodeIfPresent( - _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? - { - if let sqliteStatement = sqliteStatement { - return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) - } - return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) - } -} diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index e2f740b81b..d6693de262 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -151,7 +151,7 @@ extension Row { /// let row = try Row.fetchOne(db, "SELECT NULL, NULL")! /// row.containsNonNullValue // false public var containsNonNullValue: Bool { - for i in (0.. Int { GRDBPrecondition(index >= 0 && index < count, "row index out of range", file: file, line: line) return index } + + @inline(__always) + private func decode( + _ type: Value.Type, + atUncheckedIndex index: Int, + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + { + return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + } + + @inline(__always) + private func decodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + { + return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + } + + @inline(__always) + private func fastDecode( + _ type: Value.Type, + atUncheckedIndex index: Int, + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + { + if let sqliteStatement = sqliteStatement { + return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) + } + return impl.fastDecode(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) + } + + @inline(__always) + private func fastDecodeIfPresent( + _ type: Value.Type, + atUncheckedIndex index: Int, + conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + { + if let sqliteStatement = sqliteStatement { + return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) + } + return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) + } } extension Row { From 66541922a44d46762a8f9f620dc54c62104e36f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 12:27:47 +0200 Subject: [PATCH 24/31] More cleanup --- GRDB/Core/DatabaseValueConvertible.swift | 32 +++++-- GRDB/Core/Row.swift | 83 ++++++++----------- GRDB/Core/Statement.swift | 11 ++- GRDB/Core/StatementColumnConvertible.swift | 32 +++++-- GRDB/Record/FetchableRecord.swift | 8 +- .../DatabaseValueConvertibleFetchTests.swift | 2 + Tests/GRDBTests/RowFetchTests.swift | 1 + Tests/GRDBTests/SelectStatementTests.swift | 54 +++++++++++- ...StatementColumnConvertibleFetchTests.swift | 2 + 9 files changed, 154 insertions(+), 71 deletions(-) diff --git a/GRDB/Core/DatabaseValueConvertible.swift b/GRDB/Core/DatabaseValueConvertible.swift index e1ab8a5545..1502d8a895 100644 --- a/GRDB/Core/DatabaseValueConvertible.swift +++ b/GRDB/Core/DatabaseValueConvertible.swift @@ -55,15 +55,23 @@ public final class DatabaseValueCursor : Cursor init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { self.statement = statement - // We'll read from leftmost column at index 0, unless adapter mangles columns - self.columnIndex = try Int32(adapter?.baseColumnIndex(atIndex: 0, layout: statement) ?? 0) self.sqliteStatement = statement.sqliteStatement - statement.cursorReset(arguments: arguments) + if let adapter = adapter { + // adapter may redefine the index of the leftmost column + self.columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement)) + } else { + self.columnIndex = 0 + } + statement.reset(withArguments: arguments) } /// :nodoc: public func next() throws -> Value? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true @@ -101,15 +109,23 @@ public final class NullableDatabaseValueCursor init(statement: SelectStatement, arguments: StatementArguments? = nil, adapter: RowAdapter? = nil) throws { self.statement = statement - // We'll read from leftmost column at index 0, unless adapter mangles columns - self.columnIndex = try Int32(adapter?.baseColumnIndex(atIndex: 0, layout: statement) ?? 0) self.sqliteStatement = statement.sqliteStatement - statement.cursorReset(arguments: arguments) + if let adapter = adapter { + // adapter may redefine the index of the leftmost column + self.columnIndex = try Int32(adapter.baseColumnIndex(atIndex: 0, layout: statement)) + } else { + self.columnIndex = 0 + } + statement.reset(withArguments: arguments) } /// :nodoc: public func next() throws -> Value?? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true diff --git a/GRDB/Core/Row.swift b/GRDB/Core/Row.swift index d6693de262..daae4d4c22 100644 --- a/GRDB/Core/Row.swift +++ b/GRDB/Core/Row.swift @@ -195,10 +195,7 @@ extension Row { /// fail, a fatal error is raised. public subscript(_ index: Int) -> Value? { let index = checkedIndex(index) - return decodeIfPresent( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(index)) + return decodeIfPresent(Value.self, atUncheckedIndex: index) } /// Returns the value at given index, converted to the requested type. @@ -215,10 +212,7 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value? { let index = checkedIndex(index) - return fastDecodeIfPresent( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(index)) + return fastDecodeIfPresent(Value.self, atUncheckedIndex: index) } /// Returns the value at given index, converted to the requested type. @@ -230,10 +224,7 @@ extension Row { /// SQLite value can not be converted to `Value`. public subscript(_ index: Int) -> Value { let index = checkedIndex(index) - return decode( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(index)) + return decode(Value.self, atUncheckedIndex: index) } /// Returns the value at given index, converted to the requested type. @@ -249,10 +240,7 @@ extension Row { /// (see https://www.sqlite.org/datatype3.html). public subscript(_ index: Int) -> Value { let index = checkedIndex(index) - return fastDecode( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(index)) + return fastDecode(Value.self, atUncheckedIndex: index) } /// Returns Int64, Double, String, Data or nil, depending on the value @@ -288,10 +276,7 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return decodeIfPresent( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(columnName)) + return decodeIfPresent(Value.self, atUncheckedIndex: index) } /// Returns the value at given column, converted to the requested type. @@ -310,10 +295,7 @@ extension Row { guard let index = index(ofColumn: columnName) else { return nil } - return fastDecodeIfPresent( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(columnName)) + return fastDecodeIfPresent(Value.self, atUncheckedIndex: index) } /// Returns the value at given column, converted to the requested type. @@ -333,10 +315,7 @@ extension Row { from: nil, conversionContext: ValueConversionContext(self).atColumn(columnName)) } - return decode( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(columnName)) + return decode(Value.self, atUncheckedIndex: index) } /// Returns the value at given column, converted to the requested type. @@ -360,10 +339,7 @@ extension Row { from: nil, conversionContext: ValueConversionContext(self).atColumn(columnName)) } - return fastDecode( - Value.self, - atUncheckedIndex: index, - conversionContext: ValueConversionContext(self).atColumn(columnName)) + return fastDecode(Value.self, atUncheckedIndex: index) } /// Returns Int64, Double, String, NSData or nil, depending on the value @@ -498,43 +474,52 @@ extension Row { @inline(__always) private func decode( _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + atUncheckedIndex index: Int) -> Value { - return Value.decode(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + return Value.decode( + from: impl.databaseValue(atUncheckedIndex: index), + conversionContext: ValueConversionContext(self).atColumn(index)) } @inline(__always) private func decodeIfPresent( _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + atUncheckedIndex index: Int) -> Value? { - return Value.decodeIfPresent(from: impl.databaseValue(atUncheckedIndex: index), conversionContext: conversionContext) + return Value.decodeIfPresent( + from: impl.databaseValue(atUncheckedIndex: index), + conversionContext: ValueConversionContext(self).atColumn(index)) } @inline(__always) private func fastDecode( _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value + atUncheckedIndex index: Int) -> Value { if let sqliteStatement = sqliteStatement { - return Value.fastDecode(from: sqliteStatement, index: Int32(index), conversionContext: conversionContext) + return Value.fastDecode( + from: sqliteStatement, + index: Int32(index), + conversionContext: ValueConversionContext(self).atColumn(index)) } - return impl.fastDecode(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) + return impl.fastDecode( + Value.self, + atUncheckedIndex: index, + conversionContext: ValueConversionContext(self).atColumn(index)) } @inline(__always) private func fastDecodeIfPresent( _ type: Value.Type, - atUncheckedIndex index: Int, - conversionContext: @autoclosure () -> ValueConversionContext?) -> Value? + atUncheckedIndex index: Int) -> Value? { if let sqliteStatement = sqliteStatement { return Value.fastDecodeIfPresent(from: sqliteStatement, index: Int32(index)) } - return impl.fastDecodeIfPresent(Value.self, atUncheckedIndex: index, conversionContext: conversionContext) + return impl.fastDecodeIfPresent( + Value.self, + atUncheckedIndex: index, + conversionContext: ValueConversionContext(self).atColumn(index)) } } @@ -675,12 +660,16 @@ public final class RowCursor : Cursor { self.statement = statement self.row = try Row(statement: statement).adapted(with: adapter, layout: statement) self.sqliteStatement = statement.sqliteStatement - statement.cursorReset(arguments: arguments) + statement.reset(withArguments: arguments) } /// :nodoc: public func next() throws -> Row? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true diff --git a/GRDB/Core/Statement.swift b/GRDB/Core/Statement.swift index cb8679e053..508fc971bb 100644 --- a/GRDB/Core/Statement.swift +++ b/GRDB/Core/Statement.swift @@ -361,7 +361,7 @@ public final class SelectStatement : Statement { } /// Utility function for cursors - func cursorReset(arguments: StatementArguments? = nil) { + func reset(withArguments arguments: StatementArguments? = nil) { SchedulingWatchdog.preconditionValidQueue(database) prepare(withArguments: arguments) reset() @@ -371,6 +371,7 @@ public final class SelectStatement : Statement { // Hide AuthorizedStatement from Jazzy extension SelectStatement: AuthorizedStatement { } +// TODO: remove public qualifier, or expose SelectStatement.makeCursor() /// A cursor that iterates a database statement without producing any value. /// For example: /// @@ -387,12 +388,16 @@ public final class StatementCursor: Cursor { fileprivate init(statement: SelectStatement, arguments: StatementArguments? = nil) { self.statement = statement self.sqliteStatement = statement.sqliteStatement - statement.cursorReset(arguments: arguments) + statement.reset(withArguments: arguments) } /// :nodoc: public func next() throws -> Void? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true diff --git a/GRDB/Core/StatementColumnConvertible.swift b/GRDB/Core/StatementColumnConvertible.swift index 2ec7b052b8..46e8e8df04 100644 --- a/GRDB/Core/StatementColumnConvertible.swift +++ b/GRDB/Core/StatementColumnConvertible.swift @@ -67,15 +67,23 @@ public final class FastDatabaseValueCursor Value? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true @@ -112,15 +120,23 @@ public final class FastNullableDatabaseValueCursor Value?? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true diff --git a/GRDB/Record/FetchableRecord.swift b/GRDB/Record/FetchableRecord.swift index fa7db477ec..28d6a1e009 100644 --- a/GRDB/Record/FetchableRecord.swift +++ b/GRDB/Record/FetchableRecord.swift @@ -48,12 +48,16 @@ public final class RecordCursor : Cursor { self.statement = statement self.row = try Row(statement: statement).adapted(with: adapter, layout: statement) self.sqliteStatement = statement.sqliteStatement - statement.cursorReset(arguments: arguments) + statement.reset(withArguments: arguments) } /// :nodoc: public func next() throws -> Record? { - if done { return nil } + if done { + // make sure this instance never yields a value again, even if the + // statement is reset by another cursor. + return nil + } switch sqlite3_step(sqliteStatement) { case SQLITE_DONE: done = true diff --git a/Tests/GRDBTests/DatabaseValueConvertibleFetchTests.swift b/Tests/GRDBTests/DatabaseValueConvertibleFetchTests.swift index 8501da8f4b..ccfb8d39d1 100644 --- a/Tests/GRDBTests/DatabaseValueConvertibleFetchTests.swift +++ b/Tests/GRDBTests/DatabaseValueConvertibleFetchTests.swift @@ -35,6 +35,7 @@ class DatabaseValueConvertibleFetchTests: GRDBTestCase { XCTAssertEqual(try cursor.next()!.int, 1) XCTAssertEqual(try cursor.next()!.int, 2) XCTAssertTrue(try cursor.next() == nil) // end + XCTAssertTrue(try cursor.next() == nil) // past the end } do { let sql = "SELECT 1 UNION ALL SELECT 2" @@ -370,6 +371,7 @@ class DatabaseValueConvertibleFetchTests: GRDBTestCase { XCTAssertEqual(try cursor.next()!!.int, 1) XCTAssertTrue(try cursor.next()! == nil) XCTAssertTrue(try cursor.next() == nil) // end + XCTAssertTrue(try cursor.next() == nil) // past the end } do { let sql = "SELECT 1 UNION ALL SELECT NULL" diff --git a/Tests/GRDBTests/RowFetchTests.swift b/Tests/GRDBTests/RowFetchTests.swift index 617aba94d8..3b2857c7eb 100644 --- a/Tests/GRDBTests/RowFetchTests.swift +++ b/Tests/GRDBTests/RowFetchTests.swift @@ -28,6 +28,7 @@ class RowFetchTests: GRDBTestCase { XCTAssertEqual(row["firstName"] as String, "Barbara") XCTAssertEqual(row["lastName"] as String, "Gourde") XCTAssertTrue(try cursor.next() == nil) // end + XCTAssertTrue(try cursor.next() == nil) // past the end } do { let sql = "SELECT 'Arthur' AS firstName, 'Martin' AS lastName UNION ALL SELECT 'Barbara', 'Gourde'" diff --git a/Tests/GRDBTests/SelectStatementTests.swift b/Tests/GRDBTests/SelectStatementTests.swift index 66b6cc1baa..521770fbd0 100644 --- a/Tests/GRDBTests/SelectStatementTests.swift +++ b/Tests/GRDBTests/SelectStatementTests.swift @@ -1,15 +1,15 @@ import XCTest #if GRDBCIPHER - import GRDBCipher + @testable import GRDBCipher #elseif GRDBCUSTOMSQLITE - import GRDBCustomSQLite + @testable import GRDBCustomSQLite #else #if SWIFT_PACKAGE import CSQLite #else import SQLite3 #endif - import GRDB + @testable import GRDB #endif class SelectStatementTests : GRDBTestCase { @@ -32,6 +32,54 @@ class SelectStatementTests : GRDBTestCase { try migrator.migrate(dbWriter) } + func testStatementCursor() throws { + let dbQueue = try makeDatabaseQueue() + try dbQueue.inDatabase { db in + let sql = "SELECT 'Arthur' AS firstName, 'Martin' AS lastName UNION ALL SELECT 'Barbara', 'Gourde'" + let statement = try db.makeSelectStatement(sql) + let cursor = statement.makeCursor() + + // Check that StatementCursor gives access to the raw SQLite API + XCTAssertEqual(String(cString: sqlite3_column_name(cursor.statement.sqliteStatement, 0)), "firstName") + + XCTAssertFalse(try cursor.next() == nil) + XCTAssertFalse(try cursor.next() == nil) + XCTAssertTrue(try cursor.next() == nil) // end + XCTAssertTrue(try cursor.next() == nil) // past the end + } + } + + func testStatementCursorStepFailure() throws { + let dbQueue = try makeDatabaseQueue() + let customError = NSError(domain: "Custom", code: 0xDEAD) + dbQueue.add(function: DatabaseFunction("throw", argumentCount: 0, pure: true) { _ in throw customError }) + try dbQueue.inDatabase { db in + func test(_ cursor: StatementCursor) throws { + let sql = cursor.statement.sql + do { + _ = try cursor.next() + XCTFail() + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_ERROR) + XCTAssertEqual(error.message, "\(customError)") + XCTAssertEqual(error.sql!, sql) + XCTAssertEqual(error.description, "SQLite error 1 with statement `\(sql)`: \(customError)") + } + do { + _ = try cursor.next() + XCTFail() + } catch let error as DatabaseError { + XCTAssertEqual(error.resultCode, .SQLITE_MISUSE) + XCTAssertEqual(error.message, "\(customError)") + XCTAssertEqual(error.sql!, sql) + XCTAssertEqual(error.description, "SQLite error 21 with statement `\(sql)`: \(customError)") + } + } + try test(db.makeSelectStatement("SELECT throw(), NULL").makeCursor()) + try test(db.makeSelectStatement("SELECT 0, throw(), NULL").makeCursor()) + } + } + func testArrayStatementArguments() throws { let dbQueue = try makeDatabaseQueue() try dbQueue.inDatabase { db in diff --git a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift index 7a4f80ff06..0e65863f50 100644 --- a/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift +++ b/Tests/GRDBTests/StatementColumnConvertibleFetchTests.swift @@ -94,6 +94,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { XCTAssertEqual(i.int, 2) XCTAssertTrue(i.fast) XCTAssertTrue(try cursor.next() == nil) // end + XCTAssertTrue(try cursor.next() == nil) // past the end } do { let sql = "SELECT 1 UNION ALL SELECT 2" @@ -432,6 +433,7 @@ class StatementColumnConvertibleFetchTests: GRDBTestCase { XCTAssertTrue(i!.fast) XCTAssertTrue(try cursor.next()! == nil) XCTAssertTrue(try cursor.next() == nil) // end + XCTAssertTrue(try cursor.next() == nil) // past the end } do { let sql = "SELECT 1 UNION ALL SELECT NULL" From 5579dd651a9562a041af06d5f93c8f99b7e233b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 12:42:58 +0200 Subject: [PATCH 25/31] Perform performance tests against Realm 3.7.6 --- GRDB.xcodeproj/project.pbxproj | 8 ++++---- Makefile | 2 +- Tests/Performance/Realm | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 5678acb5a3..89cc1400c0 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -1043,8 +1043,8 @@ 56B6EF55208CB4E3002F0ACB /* ColumnExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressionTests.swift; sourceTree = ""; }; 56B7F4291BE14A1900E39BBF /* CGFloatTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGFloatTests.swift; sourceTree = ""; }; 56B7F4391BEB42D500E39BBF /* Migration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; - 56B8C2401CA1758F00510325 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = "Realm/build/osx/swift-4.1/Realm.framework"; sourceTree = ""; }; - 56B8C2411CA1758F00510325 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = "Realm/build/osx/swift-4.1/RealmSwift.framework"; sourceTree = ""; }; + 56B8C2401CA1758F00510325 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = "Realm/build/osx/swift-4.1.2/Realm.framework"; sourceTree = ""; }; + 56B8C2411CA1758F00510325 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = "Realm/build/osx/swift-4.1.2/RealmSwift.framework"; sourceTree = ""; }; 56B8F49A1B4E2F3600C24296 /* GRDB.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = GRDB.xcconfig; sourceTree = ""; }; 56B9649C1DA51B4C0002DA19 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = ""; }; 56B964B01DA51D010002DA19 /* FTS5TokenizerDescriptor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5TokenizerDescriptor.swift; sourceTree = ""; }; @@ -3074,7 +3074,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-4.1", + "$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-4.1.2", ); GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = Tests/Info.plist; @@ -3097,7 +3097,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-4.1", + "$(PROJECT_DIR)/Tests/Performance/Realm/build/osx/swift-4.1.2", ); GCC_NO_COMMON_BLOCKS = YES; INFOPLIST_FILE = Tests/Info.plist; diff --git a/Makefile b/Makefile index 97000a7de4..6857eb9aa0 100644 --- a/Makefile +++ b/Makefile @@ -319,7 +319,7 @@ test_performance: Realm FMDB SQLite.swift -scheme GRDBOSXPerformanceComparisonTests \ build-for-testing test-without-building -Realm: Tests/Performance/Realm/build/osx/swift-4.1/RealmSwift.framework +Realm: Tests/Performance/Realm/build/osx/swift-4.1.2/RealmSwift.framework # Makes sure the Tests/Performance/Realm submodule has been downloaded, and Realm framework has been built. Tests/Performance/Realm/build/osx/swift-4.1/RealmSwift.framework: diff --git a/Tests/Performance/Realm b/Tests/Performance/Realm index 13a3cb44c1..d838323410 160000 --- a/Tests/Performance/Realm +++ b/Tests/Performance/Realm @@ -1 +1 @@ -Subproject commit 13a3cb44c10436d4fb575bfcddc879698ae956e3 +Subproject commit d838323410daa177c3bf4d5ba616cf3e36605080 From b99b14eb4dbb7459fbe621ec25fa94227ff6bc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 12:45:02 +0200 Subject: [PATCH 26/31] Perform performance tests against FMDB 2.7 --- Tests/Performance/FetchNamedValuesTests.swift | 2 +- Tests/Performance/FetchPositionalValuesTests.swift | 2 +- Tests/Performance/FetchRecordStructTests.swift | 2 +- Tests/Performance/InsertNamedValuesTests.swift | 2 +- Tests/Performance/InsertPositionalValuesTests.swift | 2 +- Tests/Performance/fmdb | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/Performance/FetchNamedValuesTests.swift b/Tests/Performance/FetchNamedValuesTests.swift index f6b4b749a7..13d4111551 100644 --- a/Tests/Performance/FetchNamedValuesTests.swift +++ b/Tests/Performance/FetchNamedValuesTests.swift @@ -41,7 +41,7 @@ class FetchNamedValuesTests: XCTestCase { #if GRDB_COMPARE func testFMDB() { let databasePath = Bundle(for: type(of: self)).path(forResource: "PerformanceTests", ofType: "sqlite")! - let dbQueue = FMDatabaseQueue(path: databasePath)! + let dbQueue = FMDatabaseQueue(path: databasePath) measure { var count = 0 diff --git a/Tests/Performance/FetchPositionalValuesTests.swift b/Tests/Performance/FetchPositionalValuesTests.swift index aa3580edf2..5c364250dd 100644 --- a/Tests/Performance/FetchPositionalValuesTests.swift +++ b/Tests/Performance/FetchPositionalValuesTests.swift @@ -83,7 +83,7 @@ class FetchPositionalValuesTests: XCTestCase { #if GRDB_COMPARE func testFMDB() { let databasePath = Bundle(for: type(of: self)).path(forResource: "PerformanceTests", ofType: "sqlite")! - let dbQueue = FMDatabaseQueue(path: databasePath)! + let dbQueue = FMDatabaseQueue(path: databasePath) measure { var count = 0 diff --git a/Tests/Performance/FetchRecordStructTests.swift b/Tests/Performance/FetchRecordStructTests.swift index f52cc609e0..d3832bdece 100644 --- a/Tests/Performance/FetchRecordStructTests.swift +++ b/Tests/Performance/FetchRecordStructTests.swift @@ -86,7 +86,7 @@ class FetchRecordStructTests: XCTestCase { // Here we test the loading of an array of Records. let databasePath = Bundle(for: type(of: self)).path(forResource: "PerformanceTests", ofType: "sqlite")! - let dbQueue = FMDatabaseQueue(path: databasePath)! + let dbQueue = FMDatabaseQueue(path: databasePath) measure { var items = [ItemStruct]() diff --git a/Tests/Performance/InsertNamedValuesTests.swift b/Tests/Performance/InsertNamedValuesTests.swift index bbc12f785c..5d3fd67bc9 100644 --- a/Tests/Performance/InsertNamedValuesTests.swift +++ b/Tests/Performance/InsertNamedValuesTests.swift @@ -57,7 +57,7 @@ class InsertNamedValuesTests: XCTestCase { measure { _ = try? FileManager.default.removeItem(atPath: databasePath) - let dbQueue = FMDatabaseQueue(path: databasePath)! + let dbQueue = FMDatabaseQueue(path: databasePath) dbQueue.inDatabase { db in db.executeStatements("CREATE TABLE items (i0 INT, i1 INT, i2 INT, i3 INT, i4 INT, i5 INT, i6 INT, i7 INT, i8 INT, i9 INT)") } diff --git a/Tests/Performance/InsertPositionalValuesTests.swift b/Tests/Performance/InsertPositionalValuesTests.swift index d97da5514e..9eb938ae70 100644 --- a/Tests/Performance/InsertPositionalValuesTests.swift +++ b/Tests/Performance/InsertPositionalValuesTests.swift @@ -102,7 +102,7 @@ class InsertPositionalValuesTests: XCTestCase { measure { _ = try? FileManager.default.removeItem(atPath: databasePath) - let dbQueue = FMDatabaseQueue(path: databasePath)! + let dbQueue = FMDatabaseQueue(path: databasePath) dbQueue.inDatabase { db in db.executeStatements("CREATE TABLE items (i0 INT, i1 INT, i2 INT, i3 INT, i4 INT, i5 INT, i6 INT, i7 INT, i8 INT, i9 INT)") } diff --git a/Tests/Performance/fmdb b/Tests/Performance/fmdb index e0fcde9a0e..d02456a3c9 160000 --- a/Tests/Performance/fmdb +++ b/Tests/Performance/fmdb @@ -1 +1 @@ -Subproject commit e0fcde9a0e5868e7400032448133f929c64500d1 +Subproject commit d02456a3c96e7bba75fa19352fb3d68c6c3140d9 From 1ae8e39dab459ce6b892d418e1bf26a7c2641c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 12:46:51 +0200 Subject: [PATCH 27/31] Perform performance tests against SQLite.swift 0.11.5 --- Tests/Performance/SQLite.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Performance/SQLite.swift b/Tests/Performance/SQLite.swift index 1d2eef2570..210098546d 160000 --- a/Tests/Performance/SQLite.swift +++ b/Tests/Performance/SQLite.swift @@ -1 +1 @@ -Subproject commit 1d2eef2570c89dbfa221fc231a8af1148ea234a3 +Subproject commit 210098546d59678f720e4ef67f8b562acfebbdb0 From 6a21e97a12cb27e704eb9d1d547f74c7140ed636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 13:43:16 +0200 Subject: [PATCH 28/31] Profile struct decoding --- .../GRDBProfiling/AppDelegate.swift | 74 ++++++++++++++++++- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift b/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift index f7747d584a..44eecc8a3a 100644 --- a/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift +++ b/Tests/Performance/GRDBProfiling/GRDBProfiling/AppDelegate.swift @@ -17,6 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ aNotification: Notification) { fetchPositionalValues() fetchNamedValues() + fetchStructs() fetchRecords() insertPositionalValues() insertNamedValues() @@ -77,11 +78,23 @@ class AppDelegate: NSObject, NSApplicationDelegate { assert(count == expectedRowCount) } + func fetchStructs() { + let databasePath = Bundle(for: type(of: self)).path(forResource: "ProfilingDatabase", ofType: "sqlite")! + let dbQueue = try! DatabaseQueue(path: databasePath) + let items = dbQueue.inDatabase { db in + try! ItemStruct.fetchAll(db) + } + assert(items.count == expectedRowCount) + assert(items[0].i0 == 0) + assert(items[1].i1 == 1) + assert(items[expectedRowCount-1].i9 == expectedRowCount-1) + } + func fetchRecords() { let databasePath = Bundle(for: type(of: self)).path(forResource: "ProfilingDatabase", ofType: "sqlite")! let dbQueue = try! DatabaseQueue(path: databasePath) let items = dbQueue.inDatabase { db in - try! Item.fetchAll(db) + try! ItemRecord.fetchAll(db) } assert(items.count == expectedRowCount) assert(items[0].i0 == 0) @@ -170,15 +183,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { try! dbQueue.inTransaction { db in for i in 0.. Date: Sun, 12 Aug 2018 13:43:39 +0200 Subject: [PATCH 29/31] Fix versions in performance report --- Tests/generatePerformanceReport.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/generatePerformanceReport.rb b/Tests/generatePerformanceReport.rb index ac5bc49eb6..d362ec021f 100755 --- a/Tests/generatePerformanceReport.rb +++ b/Tests/generatePerformanceReport.rb @@ -27,8 +27,8 @@ def formatted_samples(samples, test) samples = JSON.parse(STDIN.read) grdb_version = info_plist_version('Support/Info.plist') fmdb_version = info_plist_version('Tests/Performance/fmdb/src/fmdb/Info.plist') -sqlite_swift_version = info_plist_version('Tests/Performance/SQLite.swift/Sources/SQLite/Info.plist') -realm_version = info_plist_version('Tests/Performance/Realm/build/osx/Realm.framework/Versions/A/Resources/Info.plist') +sqlite_swift_version = '0.11.5' # not up-to-date: info_plist_version('Tests/Performance/SQLite.swift/Sources/SQLite/Info.plist') +realm_version = '3.7.6' # not up-to-date: info_plist_version('Tests/Performance/Realm/build/osx/Realm.framework/Versions/A/Resources/Info.plist') puts <<-REPORT # Comparing the Performances of Swift SQLite libraries From e415a93d7e7cc82152efcd3fc076b020160940c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sun, 12 Aug 2018 14:10:38 +0200 Subject: [PATCH 30/31] Performance tests for Codable records --- GRDB.xcodeproj/project.pbxproj | 12 ++++++ .../Performance/FetchRecordCodableTests.swift | 27 +++++++++++++ .../InsertRecordCodableTests.swift | 39 +++++++++++++++++++ Tests/Performance/PerformanceTests.swift | 15 +++++++ Tests/generatePerformanceReport.rb | 18 ++++++++- 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 Tests/Performance/FetchRecordCodableTests.swift create mode 100644 Tests/Performance/InsertRecordCodableTests.swift diff --git a/GRDB.xcodeproj/project.pbxproj b/GRDB.xcodeproj/project.pbxproj index 89cc1400c0..22e81d0484 100755 --- a/GRDB.xcodeproj/project.pbxproj +++ b/GRDB.xcodeproj/project.pbxproj @@ -374,6 +374,10 @@ 56873BEC1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; }; 56873BEF1F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; }; 56873BF21F2CB400004D24B4 /* Fixits-1.2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */; }; + 5690AFD82120589A001530EA /* InsertRecordCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690AFD72120589A001530EA /* InsertRecordCodableTests.swift */; }; + 5690AFD92120589A001530EA /* InsertRecordCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690AFD72120589A001530EA /* InsertRecordCodableTests.swift */; }; + 5690AFDB212058CB001530EA /* FetchRecordCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */; }; + 5690AFDC212058CB001530EA /* FetchRecordCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */; }; 5690C32A1D23E6D800E59934 /* FoundationDateComponentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */; }; 5690C33B1D23E7D200E59934 /* FoundationDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */; }; 5690C3401D23E82A00E59934 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5690C33F1D23E82A00E59934 /* Data.swift */; }; @@ -961,6 +965,8 @@ 568068301EBBA26100EFB8AA /* SQLRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SQLRequestTests.swift; sourceTree = ""; }; 5687359E1CEDE16C009B9116 /* Betty.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Betty.jpeg; sourceTree = ""; }; 56873BEB1F2CB400004D24B4 /* Fixits-1.2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fixits-1.2.swift"; sourceTree = ""; }; + 5690AFD72120589A001530EA /* InsertRecordCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsertRecordCodableTests.swift; sourceTree = ""; }; + 5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchRecordCodableTests.swift; sourceTree = ""; }; 5690C3251D23E6D800E59934 /* FoundationDateComponentsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateComponentsTests.swift; sourceTree = ""; }; 5690C3361D23E7D200E59934 /* FoundationDateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationDateTests.swift; sourceTree = ""; }; 5690C33F1D23E82A00E59934 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; @@ -1717,10 +1723,12 @@ 56DE7B271C41302500861EB8 /* FetchNamedValuesTests.swift */, 56DE7B291C4130AF00861EB8 /* FetchPositionalValuesTests.swift */, 56DE7B2B1C41311900861EB8 /* FetchRecordClassTests.swift */, + 5690AFDA212058CB001530EA /* FetchRecordCodableTests.swift */, 56D3BE701F4EB1900034C6D2 /* FetchRecordStructTests.swift */, 56DE7B251C412FDA00861EB8 /* InsertNamedValuesTests.swift */, 56DE7B231C412F7E00861EB8 /* InsertPositionalValuesTests.swift */, 56BB86121BA9886D001F9168 /* InsertRecordClassTests.swift */, + 5690AFD72120589A001530EA /* InsertRecordCodableTests.swift */, 56D507821F6D7A4500AE1C5B /* InsertRecordStructTests.swift */, 56CA22211BB41565009A04C5 /* PerformanceTests.swift */, 56B8C2401CA1758F00510325 /* Realm.framework */, @@ -2278,6 +2286,8 @@ 56707201208A509C006AD95A /* DateParsingTests.swift in Sources */, 56DE7B2C1C41311900861EB8 /* FetchRecordClassTests.swift in Sources */, 56DE7B281C41302500861EB8 /* FetchNamedValuesTests.swift in Sources */, + 5690AFD82120589A001530EA /* InsertRecordCodableTests.swift in Sources */, + 5690AFDB212058CB001530EA /* FetchRecordCodableTests.swift in Sources */, 56D507831F6D7B2E00AE1C5B /* InsertRecordStructTests.swift in Sources */, 56DE7B241C412F7E00861EB8 /* InsertPositionalValuesTests.swift in Sources */, 560C98241C0E23BB00BF8471 /* PerformanceTests.swift in Sources */, @@ -2300,10 +2310,12 @@ 56D507841F6D7B2F00AE1C5B /* InsertRecordStructTests.swift in Sources */, 56439B3A1F4CA1DC0066043F /* FMDatabaseQueue.m in Sources */, 56439B3B1F4CA1DC0066043F /* FMDatabasePool.m in Sources */, + 5690AFD92120589A001530EA /* InsertRecordCodableTests.swift in Sources */, 56439B3C1F4CA1DC0066043F /* PerformanceTests.swift in Sources */, 56D3BE721F4EB1A00034C6D2 /* FetchRecordStructTests.swift in Sources */, 56439B3D1F4CA1DC0066043F /* InsertNamedValuesTests.swift in Sources */, 56439B3E1F4CA1DC0066043F /* PerformanceModel.xcdatamodeld in Sources */, + 5690AFDC212058CB001530EA /* FetchRecordCodableTests.swift in Sources */, 56439B3F1F4CA1DC0066043F /* FetchPositionalValuesTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Tests/Performance/FetchRecordCodableTests.swift b/Tests/Performance/FetchRecordCodableTests.swift new file mode 100644 index 0000000000..6cf76cec45 --- /dev/null +++ b/Tests/Performance/FetchRecordCodableTests.swift @@ -0,0 +1,27 @@ +import XCTest +import SQLite3 +import GRDB +#if GRDB_COMPARE +import SQLite +#endif + +private let expectedRowCount = 100_000 + +/// Here we test the extraction of models from rows +class FetchRecordCodableTests: XCTestCase { + + func testGRDB() throws { + let databasePath = Bundle(for: type(of: self)).path(forResource: "PerformanceTests", ofType: "sqlite")! + let dbQueue = try DatabaseQueue(path: databasePath) + + measure { + let items = try! dbQueue.inDatabase { db in + try ItemCodable.fetchAll(db, "SELECT * FROM items") + } + XCTAssertEqual(items.count, expectedRowCount) + XCTAssertEqual(items[0].i0, 0) + XCTAssertEqual(items[1].i1, 1) + XCTAssertEqual(items[expectedRowCount-1].i9, expectedRowCount-1) + } + } +} diff --git a/Tests/Performance/InsertRecordCodableTests.swift b/Tests/Performance/InsertRecordCodableTests.swift new file mode 100644 index 0000000000..b33be57dc3 --- /dev/null +++ b/Tests/Performance/InsertRecordCodableTests.swift @@ -0,0 +1,39 @@ +import XCTest +import GRDB + +private let insertedRowCount = 20_000 + +// Here we insert records. +class InsertRecordCodableTests: XCTestCase { + + func testGRDB() { + let databaseFileName = "GRDBPerformanceTests-\(ProcessInfo.processInfo.globallyUniqueString).sqlite" + let databasePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(databaseFileName) + _ = try? FileManager.default.removeItem(atPath: databasePath) + defer { + let dbQueue = try! DatabaseQueue(path: databasePath) + try! dbQueue.inDatabase { db in + XCTAssertEqual(try Int.fetchOne(db, "SELECT COUNT(*) FROM items")!, insertedRowCount) + XCTAssertEqual(try Int.fetchOne(db, "SELECT MIN(i0) FROM items")!, 0) + XCTAssertEqual(try Int.fetchOne(db, "SELECT MAX(i9) FROM items")!, insertedRowCount - 1) + } + try! FileManager.default.removeItem(atPath: databasePath) + } + + measure { + _ = try? FileManager.default.removeItem(atPath: databasePath) + + let dbQueue = try! DatabaseQueue(path: databasePath) + try! dbQueue.inDatabase { db in + try db.execute("CREATE TABLE items (i0 INT, i1 INT, i2 INT, i3 INT, i4 INT, i5 INT, i6 INT, i7 INT, i8 INT, i9 INT)") + } + + try! dbQueue.inTransaction { db in + for i in 0.. Date: Sun, 12 Aug 2018 15:12:46 +0200 Subject: [PATCH 31/31] The final touch --- CHANGELOG.md | 8 +----- GRDB/Core/DatabaseValue.swift | 8 +++--- GRDB/Core/DatabaseValueConversion.swift | 10 ++++++- .../DatabaseValueConversionErrorTests.swift | 26 +++++++++---------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54750e3954..7237c0590f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,19 +3,13 @@ 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 - [#393](https://github.com/groue/GRDB.swift/pull/393): Upgrade SQLCipher to 3.4.2, enable FTS5 on GRDBCipher and new pod GRDBPlus. ### API diff ```diff - extension DatabaseValue { -+ @available(*, deprecated) - func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T -+ @available(*, deprecated) - func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? - } - +final class FastDatabaseValueCursor : Cursor { } +@available(*, deprecated, renamed: "FastDatabaseValueCursor") +typealias ColumnCursor = FastDatabaseValueCursor diff --git a/GRDB/Core/DatabaseValue.swift b/GRDB/Core/DatabaseValue.swift index 8b682e5230..acd23942ec 100644 --- a/GRDB/Core/DatabaseValue.swift +++ b/GRDB/Core/DatabaseValue.swift @@ -207,6 +207,7 @@ extension DatabaseValue { // MARK: - Lossless conversions extension DatabaseValue { + // TODO: deprecate and rename to DatabaseValue.decode(_:sql:arguments:) /// Converts the database value to the type T. /// /// let dbValue = "foo".databaseValue @@ -225,11 +226,11 @@ extension DatabaseValue { /// conversion error /// - arguments: Optional statement arguments that enhances the eventual /// conversion error - @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T where T : DatabaseValueConvertible { - return T.decode(from: self, conversionContext: nil) + return T.decode(from: self, conversionContext: sql.map { ValueConversionContext(sql: $0, arguments: arguments) }) } + // TODO: deprecate and rename to DatabaseValue.decodeIfPresent(_:sql:arguments:) /// Converts the database value to the type Optional. /// /// let dbValue = "foo".databaseValue @@ -249,9 +250,8 @@ extension DatabaseValue { /// conversion error /// - arguments: Optional statement arguments that enhances the eventual /// conversion error - @available(*, deprecated) public func losslessConvert(sql: String? = nil, arguments: StatementArguments? = nil) -> T? where T : DatabaseValueConvertible { - return T.decodeIfPresent(from: self, conversionContext: nil) + return T.decodeIfPresent(from: self, conversionContext: sql.map { ValueConversionContext(sql: $0, arguments: arguments) }) } } diff --git a/GRDB/Core/DatabaseValueConversion.swift b/GRDB/Core/DatabaseValueConversion.swift index 15ca1093fb..8b4252fa24 100644 --- a/GRDB/Core/DatabaseValueConversion.swift +++ b/GRDB/Core/DatabaseValueConversion.swift @@ -76,6 +76,14 @@ extension ValueConversionContext { column: nil) } } + + init(sql: String, arguments: StatementArguments?) { + self.init( + row: nil, + sql: sql, + arguments: arguments, + column: nil) + } } /// The canonical conversion error message @@ -105,7 +113,7 @@ func conversionErrorMessage(to: T.Type, from dbValue: DatabaseValue?, convers } if let sql = conversionContext?.sql { - extras.append("statement: `\(sql)`") + extras.append("sql: `\(sql)`") if let arguments = conversionContext?.arguments, arguments.isEmpty == false { extras.append("arguments: \(arguments)") } diff --git a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift index 8a7d91a0b5..b0fdb01f7c 100644 --- a/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift +++ b/Tests/GRDBTests/DatabaseValueConversionErrorTests.swift @@ -42,7 +42,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: row["name"], conversionContext: ValueConversionContext(row).atColumn("name")), - "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL], statement: `SELECT ? AS name`, arguments: [NULL])") + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL], sql: `SELECT ? AS name`, arguments: [NULL])") } } @@ -67,7 +67,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: row["name"], conversionContext: ValueConversionContext(row).atColumn("name")), - "could not read String from missing column `name` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + "could not read String from missing column `name` (row: [unused:\"ignored\"], sql: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -108,7 +108,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: row["value"], conversionContext: ValueConversionContext(row).atColumn("value")), - "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"], statement: `SELECT 1, ? AS value`, arguments: [\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [1:1 value:\"invalid\"], sql: `SELECT 1, ? AS value`, arguments: [\"invalid\"])") } } @@ -133,7 +133,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: row["value"], conversionContext: ValueConversionContext(row).atColumn("value")), - "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], sql: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -167,7 +167,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: row["name"], conversionContext: ValueConversionContext(row).atColumn("name")), - "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], sql: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } } @@ -192,7 +192,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: row["name"], conversionContext: ValueConversionContext(row).atColumn("name")), - "could not read String from missing column `name` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + "could not read String from missing column `name` (row: [unused:\"ignored\"], sql: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -229,7 +229,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: row["value"], conversionContext: ValueConversionContext(row).atColumn("value")), - "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], sql: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") } } @@ -254,7 +254,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: row["value"], conversionContext: ValueConversionContext(row).atColumn("value")), - "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], sql: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -291,7 +291,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: row["value"], conversionContext: ValueConversionContext(row).atColumn("value")), - "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], statement: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `value`, column index: 1, row: [name:NULL value:\"invalid\"], sql: `SELECT NULL AS name, ? AS value`, arguments: [\"invalid\"])") } } @@ -316,7 +316,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: row["value"], conversionContext: ValueConversionContext(row).atColumn("value")), - "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], statement: `SELECT ? AS unused`, arguments: [\"ignored\"])") + "could not read \(Value.self) from missing column `value` (row: [unused:\"ignored\"], sql: `SELECT ? AS unused`, arguments: [\"ignored\"])") } } } @@ -337,7 +337,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: String.self, from: .null, conversionContext: ValueConversionContext(statement).atColumn(0)), - "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value NULL to String (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], sql: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } // _ = row["name"] as String @@ -378,7 +378,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: 0), conversionContext: ValueConversionContext(statement).atColumn(0)), - "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value NULL to \(Value.self) (column: `name`, column index: 0, row: [name:NULL team:\"invalid\"], sql: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } // _ = try Value.fetchOne(statement, adapter: SuffixRowAdapter(fromIndex: 1)) @@ -390,7 +390,7 @@ class DatabaseValueConversionErrorTests: GRDBTestCase { to: Value.self, from: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: Int32(columnIndex)), conversionContext: ValueConversionContext(statement).atColumn(columnIndex)), - "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], statement: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") + "could not convert database value \"invalid\" to \(Value.self) (column: `team`, column index: 1, row: [name:NULL team:\"invalid\"], sql: `SELECT NULL AS name, ? AS team`, arguments: [\"invalid\"])") } // _ = row["name"] as Value