From fd040996b18eab34603e682d92c4199465736bfd Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 14 May 2020 15:08:42 -0400 Subject: [PATCH 1/8] SQLStatement.execute() now returns a statement --- CHANGELOG.md | 7 +++++++ Sources/SwiftSQL/SQLStatement.swift | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b75d01..280a4fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # SwiftSQL 0.x +## SwiftSQL 0.2.0 + +*May 14, 2020* + +- `SQLStatement.execute()` now returns a statement so that you could chain it with `column(at:)` and other calls. + + ## SwiftSQL 0.1.0 *May 13, 2020* diff --git a/Sources/SwiftSQL/SQLStatement.swift b/Sources/SwiftSQL/SQLStatement.swift index 62397db..926bed3 100644 --- a/Sources/SwiftSQL/SQLStatement.swift +++ b/Sources/SwiftSQL/SQLStatement.swift @@ -84,8 +84,9 @@ public final class SQLStatement { /// /// - note: See [SQLite: Result and Error Codes](https://www.sqlite.org/rescode.html) /// for more information. - public func execute() throws { + public func execute() throws -> SQLStatement { try isOK(sqlite3_step(ref)) + return self } // MARK: Binding Parameters From 7c491836dd2edab9207260a44cbb0a8b1028cf51 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Wed, 20 May 2020 09:05:34 +0200 Subject: [PATCH 2/8] add the ability to subscript SQLRow by column name --- Sources/SwiftSQL/SQLStatement.swift | 15 +++++ Sources/SwiftSQLExt/SwiftSQLExt.swift | 29 +++++++++ Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift | 60 +++++++++++++++++-- 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftSQL/SQLStatement.swift b/Sources/SwiftSQL/SQLStatement.swift index 926bed3..5e060f8 100644 --- a/Sources/SwiftSQL/SQLStatement.swift +++ b/Sources/SwiftSQL/SQLStatement.swift @@ -242,6 +242,21 @@ public final class SQLStatement { public func columnName(at index: Int) -> String { String(cString: sqlite3_column_name(ref, Int32(index))) } + + // MARK: Indices from Names + + /// Returns the index of a column given its name. + public func columnIndex(forName name: String) -> Int? { + return columnIndices[name] + } + private lazy var columnIndices: [String : Int] = { + var indices: [String : Int] = [:] + indices.reserveCapacity(columnCount) + for index in 0..(_ type: T.Type, count: Int? = nil) throws -> [T] { var objects = [T]() let limit = count ?? Int.max + if let count = count { + objects.reserveCapacity(count) + } while let object = try row(T.self), objects.count < limit { objects.append(object) } @@ -60,6 +63,32 @@ public struct SQLRow { public subscript(index: Int) -> T? { statement.column(at: index) } + + /// Returns a single column (by its name) of the current result row of a query. + /// + /// If the SQL statement does not currently point to a valid row, the result is undefined. + /// If the passed columnName doesn't point to a valid column name, a fatal error is raised. + /// + /// - parameter columnName: The name of the column. + public subscript(columnName: String) -> T { + guard let columnIndex = statement.columnIndex(forName: columnName) else { + fatalError("No such column \(columnName)") + } + return statement.column(at: columnIndex) + } + + /// Returns a single column (by its name) of the current result row of a query. + /// + /// If the SQL statement does not currently point to a valid row, the result is undefined. + /// If the passed columnName doesn't point to a valid column name, nil is returned. + /// + /// - parameter columnName: The name of the column. + public subscript(columnName: String) -> T? { + guard let columnIndex = statement.columnIndex(forName: columnName) else { + return nil + } + return statement.column(at: columnIndex) + } } public protocol SQLRowDecodable { diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift index 08df71d..3d4d496 100644 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift @@ -69,6 +69,21 @@ final class SwiftSQLExtTests: XCTestCase { User(name: "Alice", level: 80) ]) } + + func testNamedSubscriptsSuccess() throws { + // GIVEN + try db.populateStore() + + // WHEN + let persons = try db + .prepare("SELECT Name FROM Persons ORDER BY Level ASC") + .rows(Person.self, count: 1) + + // THEN + XCTAssertEqual(persons, [ + Person(name: "Alice", level: nil) + ]) + } } private extension SQLConnection { @@ -81,19 +96,42 @@ private extension SQLConnection { Level INTEGER ) """) + try execute(""" + CREATE TABLE Persons + ( + Id INTEGER PRIMARY KEY NOT NULL, + Name VARCHAR, + Level INTEGER + ) + """) - let statement = try self.prepare(""" + let insertUsersStatement = try self.prepare(""" INSERT INTO Users (Name, Level) VALUES (?, ?) """) - try statement + try insertUsersStatement .bind("Alice", Int64(80)) .execute() - try statement.reset() + try insertUsersStatement.reset() - try statement + try insertUsersStatement + .bind("Bob", Int64(90)) + .execute() + + let insertPersonsStatement = try self.prepare(""" + INSERT INTO Persons (Name, Level) + VALUES (?, ?) + """) + + try insertPersonsStatement + .bind("Alice", Int64(80)) + .execute() + + try insertPersonsStatement.reset() + + try insertPersonsStatement .bind("Bob", Int64(90)) .execute() } @@ -114,3 +152,17 @@ private struct User: Hashable, SQLRowDecodable { self.level = row[1] } } +private struct Person: Hashable, SQLRowDecodable { + let name: String + let level: Int64? + + init(name: String, level: Int64?) { + self.name = name + self.level = level + } + + init(row: SQLRow) throws { + self.name = row["Name"] + self.level = row["Level"] + } +} From e1d010ba700322281cc3e61c02de836e3e7670a8 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Tue, 26 May 2020 15:57:28 +0200 Subject: [PATCH 3/8] SQLRow true value semantics with Any --- Sources/SwiftSQL/SQLDataType.swift | 4 ++ Sources/SwiftSQL/SQLStatement.swift | 22 ++++++++ Sources/SwiftSQLExt/SwiftSQLExt.swift | 51 ++++++++++++++----- Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift | 40 +++++++++++++-- 4 files changed, 99 insertions(+), 18 deletions(-) diff --git a/Sources/SwiftSQL/SQLDataType.swift b/Sources/SwiftSQL/SQLDataType.swift index d033765..9a3be00 100644 --- a/Sources/SwiftSQL/SQLDataType.swift +++ b/Sources/SwiftSQL/SQLDataType.swift @@ -14,6 +14,10 @@ public protocol SQLDataType { static func sqlColumn(statement: OpaquePointer, index: Int32) -> Self } +public extension SQLDataType { + static func convert(from value: Any) -> Self? { value as? Self } +} + extension Int: SQLDataType { public func sqlBind(statement: OpaquePointer, index: Int32) { sqlite3_bind_int64(statement, index, Int64(self)) diff --git a/Sources/SwiftSQL/SQLStatement.swift b/Sources/SwiftSQL/SQLStatement.swift index 5e060f8..cb0e6bb 100644 --- a/Sources/SwiftSQL/SQLStatement.swift +++ b/Sources/SwiftSQL/SQLStatement.swift @@ -223,6 +223,28 @@ public final class SQLStatement { return T.sqlColumn(statement: ref, index: Int32(index)) } } + + public func column(at index: Int) -> Any? { + let index = Int32(index) + let type = sqlite3_column_type(ref, index) + switch type { + case SQLITE_INTEGER: + return sqlite3_column_int64(ref, index) + case SQLITE_FLOAT: + return sqlite3_column_double(ref, index) + case SQLITE_TEXT: + return String(cString: sqlite3_column_text(ref, index)) + case SQLITE_BLOB: + if let bytes = sqlite3_column_blob(ref, index) { + let byteCount = sqlite3_column_bytes(ref, index) + return Data(bytes: bytes, count: Int(byteCount)) + } else { + return Data() + } + default: + return nil + } + } /// Return the number of columns in the result set returned by the statement. /// diff --git a/Sources/SwiftSQLExt/SwiftSQLExt.swift b/Sources/SwiftSQLExt/SwiftSQLExt.swift index 04c1cc7..49bb7e7 100644 --- a/Sources/SwiftSQLExt/SwiftSQLExt.swift +++ b/Sources/SwiftSQLExt/SwiftSQLExt.swift @@ -28,19 +28,40 @@ public extension SQLStatement { } return objects } + + /// Fetches the next row as `SQLRow`. + func row() throws -> SQLRow? { + guard try step() else { + return nil + } + return SQLRow(statement: self) + } + + /// Fetches the first `count` rows as `SQLRow`s returned by the statement. By default, + /// fetches all rows. + func rows(count: Int? = nil) throws -> [SQLRow] { + var objects = [SQLRow]() + let limit = count ?? Int.max + if let count = count { + objects.reserveCapacity(count) + } + while let object = try row(), objects.count < limit { + objects.append(object) + } + return objects + } } /// Represents a single row returned by the SQL statement. -/// -/// - warning: This is a leaky abstraction. This is not a real value type, it -/// just wraps the underlying statement. If the statement moves to the next -/// row by calling `step()`, the row is also going to point to the new row. public struct SQLRow { - /// The underlying statement. - public let statement: SQLStatement // Storing as strong reference doesn't seem to affect performance public init(statement: SQLStatement) { - self.statement = statement + values = (0..(index: Int) -> T { - statement.column(at: index) + T.convert(from: values[index]!)! } /// Returns a single column of the current result row of a query. If the @@ -61,7 +82,8 @@ public struct SQLRow { /// /// - parameter index: The leftmost column of the result set has the index 0. public subscript(index: Int) -> T? { - statement.column(at: index) + guard let value = values[index] else { return nil } + return T.convert(from: value) } /// Returns a single column (by its name) of the current result row of a query. @@ -71,10 +93,10 @@ public struct SQLRow { /// /// - parameter columnName: The name of the column. public subscript(columnName: String) -> T { - guard let columnIndex = statement.columnIndex(forName: columnName) else { + guard let columnIndex = columnIndicesByNames[columnName] else { fatalError("No such column \(columnName)") } - return statement.column(at: columnIndex) + return self[columnIndex] } /// Returns a single column (by its name) of the current result row of a query. @@ -84,11 +106,14 @@ public struct SQLRow { /// /// - parameter columnName: The name of the column. public subscript(columnName: String) -> T? { - guard let columnIndex = statement.columnIndex(forName: columnName) else { + guard let columnIndex = columnIndicesByNames[columnName] else { return nil } - return statement.column(at: columnIndex) + return self[columnIndex] } + + private let values: [Any?] + private let columnIndicesByNames: [String : Int] } public protocol SQLRowDecodable { diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift index 3d4d496..af30830 100644 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift @@ -70,20 +70,50 @@ final class SwiftSQLExtTests: XCTestCase { ]) } - func testNamedSubscriptsSuccess() throws { + func testIndependentSQLRows() throws { // GIVEN try db.populateStore() // WHEN - let persons = try db + let names: [String] = try db .prepare("SELECT Name FROM Persons ORDER BY Level ASC") - .rows(Person.self, count: 1) + .rows() + .map({ $0["Name"] }) // THEN - XCTAssertEqual(persons, [ - Person(name: "Alice", level: nil) + XCTAssertEqual(names, [ + "Alice", + "Bob" ]) } + + func testIndependentSingleSQLRowNonNil() throws { + // GIVEN + try db.populateStore() + + // WHEN + let row = try db + .prepare("SELECT Name FROM Persons ORDER BY Level ASC") + .row() + + // THEN + XCTAssertEqual(try XCTUnwrap(row)["Name"] as String, "Alice") + } + + func testIndependentSingleSQLRowNil() throws { + // GIVEN + try db.populateStore() + + // WHEN + let row = try db + .prepare("SELECT Name FROM Persons ORDER BY Level ASC") + .row() + + // THEN + XCTAssertNil(try XCTUnwrap(row)["Level"] as Int?) + } + + } private extension SQLConnection { From ac9d2b672e7d5271dcb1f6aecb0068188384d214 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Wed, 3 Jun 2020 22:27:17 +0200 Subject: [PATCH 4/8] Int type tolerance and better SQLDataType conversion error message --- Sources/SwiftSQL/SQLDataType.swift | 6 ++++++ Sources/SwiftSQLExt/SwiftSQLExt.swift | 6 +++++- Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftSQL/SQLDataType.swift b/Sources/SwiftSQL/SQLDataType.swift index 9a3be00..4c667fb 100644 --- a/Sources/SwiftSQL/SQLDataType.swift +++ b/Sources/SwiftSQL/SQLDataType.swift @@ -12,6 +12,7 @@ import SQLite3 public protocol SQLDataType { func sqlBind(statement: OpaquePointer, index: Int32) static func sqlColumn(statement: OpaquePointer, index: Int32) -> Self + static func convert(from value: Any) -> Self? } public extension SQLDataType { @@ -26,6 +27,11 @@ extension Int: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int { Int(sqlite3_column_int64(statement, index)) } + + public static func convert(from value: Any) -> Int? { + guard let int64 = value as? Int64 else { return nil } + return Int(int64) + } } extension Int32: SQLDataType { diff --git a/Sources/SwiftSQLExt/SwiftSQLExt.swift b/Sources/SwiftSQLExt/SwiftSQLExt.swift index 49bb7e7..97a68e1 100644 --- a/Sources/SwiftSQLExt/SwiftSQLExt.swift +++ b/Sources/SwiftSQLExt/SwiftSQLExt.swift @@ -71,7 +71,11 @@ public struct SQLRow { /// /// - parameter index: The leftmost column of the result set has the index 0. public subscript(index: Int) -> T { - T.convert(from: values[index]!)! + let value = values[index]! + guard let convertedValue = T.convert(from: value) else { + fatalError("Could not convert \(type(of: value)). Make sure target type (\(T.self)) correctly implements convert(from:).") + } + return convertedValue } /// Returns a single column of the current result row of a query. If the diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift index af30830..86b4b97 100644 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift @@ -170,9 +170,9 @@ private extension SQLConnection { private struct User: Hashable, SQLRowDecodable { let name: String - let level: Int64 + let level: Int - init(name: String, level: Int64) { + init(name: String, level: Int) { self.name = name self.level = level } From dbf2db4c86ff195c3b6a27f0306ea57b7976926b Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Wed, 3 Jun 2020 22:56:30 +0200 Subject: [PATCH 5/8] remove problematic default convert(from:) from SQLDataType and implement it in each type instead --- Sources/SwiftSQL/SQLDataType.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftSQL/SQLDataType.swift b/Sources/SwiftSQL/SQLDataType.swift index 4c667fb..25b1e66 100644 --- a/Sources/SwiftSQL/SQLDataType.swift +++ b/Sources/SwiftSQL/SQLDataType.swift @@ -15,10 +15,6 @@ public protocol SQLDataType { static func convert(from value: Any) -> Self? } -public extension SQLDataType { - static func convert(from value: Any) -> Self? { value as? Self } -} - extension Int: SQLDataType { public func sqlBind(statement: OpaquePointer, index: Int32) { sqlite3_bind_int64(statement, index, Int64(self)) @@ -42,6 +38,11 @@ extension Int32: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int32 { sqlite3_column_int(statement, index) } + + public static func convert(from value: Any) -> Self? { + guard let int64 = value as? Int64 else { return nil } + return Int32(int64) + } } extension Int64: SQLDataType { @@ -52,6 +53,8 @@ extension Int64: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int64 { sqlite3_column_int64(statement, index) } + + public static func convert(from value: Any) -> Self? { value as? Self } } extension Double: SQLDataType { @@ -62,6 +65,8 @@ extension Double: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Double { sqlite3_column_double(statement, index) } + + public static func convert(from value: Any) -> Self? { value as? Self } } extension String: SQLDataType { @@ -73,6 +78,8 @@ extension String: SQLDataType { guard let pointer = sqlite3_column_text(statement, index) else { return "" } return String(cString: pointer) } + + public static func convert(from value: Any) -> Self? { value as? Self } } extension Data: SQLDataType { @@ -87,6 +94,8 @@ extension Data: SQLDataType { let count = Int(sqlite3_column_bytes(statement, Int32(index))) return Data(bytes: pointer, count: count) } + + public static func convert(from value: Any) -> Self? { value as? Self } } private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) From c431a4c97a8275e2f2a0ed392fb5ececc9378b83 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Wed, 3 Jun 2020 23:05:43 +0200 Subject: [PATCH 6/8] use Int instead of Int64 in tests inserts --- Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift index 86b4b97..633bde8 100644 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift @@ -141,13 +141,13 @@ private extension SQLConnection { """) try insertUsersStatement - .bind("Alice", Int64(80)) + .bind("Alice", 80) .execute() try insertUsersStatement.reset() try insertUsersStatement - .bind("Bob", Int64(90)) + .bind("Bob", 90) .execute() let insertPersonsStatement = try self.prepare(""" From 096d382948dcc33e51ec6ced784f7b0800b4fa05 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Sat, 6 Jun 2020 12:16:09 +0200 Subject: [PATCH 7/8] introduced SQLColumnValue, removed convert(from:) from SQLDataType, and used InitializableBySQLColumnValue for SwiftSQLExt conveniences --- Sources/SwiftSQL/SQLColumnValue.swift | 13 ++ Sources/SwiftSQL/SQLDataType.swift | 19 --- Sources/SwiftSQL/SQLStatement.swift | 14 +-- ...LColumnValue+ConvenienceConformances.swift | 116 ++++++++++++++++++ Sources/SwiftSQLExt/SwiftSQLExt.swift | 21 ++-- Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift | 49 ++++++++ 6 files changed, 197 insertions(+), 35 deletions(-) create mode 100644 Sources/SwiftSQL/SQLColumnValue.swift create mode 100644 Sources/SwiftSQLExt/InitializableBySQLColumnValue+ConvenienceConformances.swift diff --git a/Sources/SwiftSQL/SQLColumnValue.swift b/Sources/SwiftSQL/SQLColumnValue.swift new file mode 100644 index 0000000..9fdb017 --- /dev/null +++ b/Sources/SwiftSQL/SQLColumnValue.swift @@ -0,0 +1,13 @@ +// The MIT License (MIT) +// +// Copyright (c) 2020 Alexander Grebenyuk (github.com/kean). + +import Foundation + +public enum SQLColumnValue { + case int64(Int64) + case double(Double) + case string(String) + case data(Data) + case null +} diff --git a/Sources/SwiftSQL/SQLDataType.swift b/Sources/SwiftSQL/SQLDataType.swift index 25b1e66..d033765 100644 --- a/Sources/SwiftSQL/SQLDataType.swift +++ b/Sources/SwiftSQL/SQLDataType.swift @@ -12,7 +12,6 @@ import SQLite3 public protocol SQLDataType { func sqlBind(statement: OpaquePointer, index: Int32) static func sqlColumn(statement: OpaquePointer, index: Int32) -> Self - static func convert(from value: Any) -> Self? } extension Int: SQLDataType { @@ -23,11 +22,6 @@ extension Int: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int { Int(sqlite3_column_int64(statement, index)) } - - public static func convert(from value: Any) -> Int? { - guard let int64 = value as? Int64 else { return nil } - return Int(int64) - } } extension Int32: SQLDataType { @@ -38,11 +32,6 @@ extension Int32: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int32 { sqlite3_column_int(statement, index) } - - public static func convert(from value: Any) -> Self? { - guard let int64 = value as? Int64 else { return nil } - return Int32(int64) - } } extension Int64: SQLDataType { @@ -53,8 +42,6 @@ extension Int64: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int64 { sqlite3_column_int64(statement, index) } - - public static func convert(from value: Any) -> Self? { value as? Self } } extension Double: SQLDataType { @@ -65,8 +52,6 @@ extension Double: SQLDataType { public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Double { sqlite3_column_double(statement, index) } - - public static func convert(from value: Any) -> Self? { value as? Self } } extension String: SQLDataType { @@ -78,8 +63,6 @@ extension String: SQLDataType { guard let pointer = sqlite3_column_text(statement, index) else { return "" } return String(cString: pointer) } - - public static func convert(from value: Any) -> Self? { value as? Self } } extension Data: SQLDataType { @@ -94,8 +77,6 @@ extension Data: SQLDataType { let count = Int(sqlite3_column_bytes(statement, Int32(index))) return Data(bytes: pointer, count: count) } - - public static func convert(from value: Any) -> Self? { value as? Self } } private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) diff --git a/Sources/SwiftSQL/SQLStatement.swift b/Sources/SwiftSQL/SQLStatement.swift index cb0e6bb..f935d6e 100644 --- a/Sources/SwiftSQL/SQLStatement.swift +++ b/Sources/SwiftSQL/SQLStatement.swift @@ -224,25 +224,25 @@ public final class SQLStatement { } } - public func column(at index: Int) -> Any? { + public func column(at index: Int) -> SQLColumnValue { let index = Int32(index) let type = sqlite3_column_type(ref, index) switch type { case SQLITE_INTEGER: - return sqlite3_column_int64(ref, index) + return .int64(sqlite3_column_int64(ref, index)) case SQLITE_FLOAT: - return sqlite3_column_double(ref, index) + return .double(sqlite3_column_double(ref, index)) case SQLITE_TEXT: - return String(cString: sqlite3_column_text(ref, index)) + return .string(String(cString: sqlite3_column_text(ref, index))) case SQLITE_BLOB: if let bytes = sqlite3_column_blob(ref, index) { let byteCount = sqlite3_column_bytes(ref, index) - return Data(bytes: bytes, count: Int(byteCount)) + return .data(Data(bytes: bytes, count: Int(byteCount))) } else { - return Data() + return .data(Data()) } default: - return nil + return .null } } diff --git a/Sources/SwiftSQLExt/InitializableBySQLColumnValue+ConvenienceConformances.swift b/Sources/SwiftSQLExt/InitializableBySQLColumnValue+ConvenienceConformances.swift new file mode 100644 index 0000000..dac22a3 --- /dev/null +++ b/Sources/SwiftSQLExt/InitializableBySQLColumnValue+ConvenienceConformances.swift @@ -0,0 +1,116 @@ +// The MIT License (MIT) +// +// Copyright (c) 2020 Alexander Grebenyuk (github.com/kean). + +import Foundation +import SwiftSQL + +// Conveniences for commonly used types. +// Note that automatc type conversion is being done; +// this is to maintain the expected type flexibility +// of SQLite. +// From the docs: +// >If the result column is not initially in the requested format (for example, if the query returns an integer but the sqlite3_column_text() interface is used to extract the value) then an automatic type conversion is performed. +// Reference: https://www.sqlite.org/c3ref/column_blob.html + +extension Int: InitializableBySQLColumnValue { + public init?(sqlColumnValue: SQLColumnValue) { + switch sqlColumnValue { + case .int64(let int64): + self = Int(int64) + case .double(let double): + self = Int(double) + case .string(let string): + if let int = Int(string) { + self = int + } else { + return nil + } + default: + return nil + } + } +} + +extension Int32: InitializableBySQLColumnValue { + public init?(sqlColumnValue: SQLColumnValue) { + switch sqlColumnValue { + case .int64(let int64): + self = Int32(int64) + case .double(let double): + self = Int32(double) + case .string(let string): + if let int32 = Int32(string) { + self = int32 + } else { + return nil + } + default: + return nil + } + } +} + +extension Int64: InitializableBySQLColumnValue { + public init?(sqlColumnValue: SQLColumnValue) { + switch sqlColumnValue { + case .int64(let int64): + self = int64 + case .double(let double): + self = Int64(double) + case .string(let string): + if let int64 = Int64(string) { + self = int64 + } else { + return nil + } + default: + return nil + } + } +} + +extension Double: InitializableBySQLColumnValue { + public init?(sqlColumnValue: SQLColumnValue) { + switch sqlColumnValue { + case .int64(let int64): + self = Double(int64) + case .double(let double): + self = double + case .string(let string): + if let double = Double(string) { + self = double + } else { + return nil + } + default: + return nil + } + } +} + +extension String: InitializableBySQLColumnValue { + public init?(sqlColumnValue: SQLColumnValue) { + switch sqlColumnValue { + case .int64(let int64): + self = String(int64) + case .double(let double): + self = String(double) + case .string(let string): + self = string + default: + return nil + } + } +} + +extension Data: InitializableBySQLColumnValue { + public init?(sqlColumnValue: SQLColumnValue) { + switch sqlColumnValue { + case .data(let data): + self = data + default: + return nil + } + } +} diff --git a/Sources/SwiftSQLExt/SwiftSQLExt.swift b/Sources/SwiftSQLExt/SwiftSQLExt.swift index 97a68e1..2edd826 100644 --- a/Sources/SwiftSQLExt/SwiftSQLExt.swift +++ b/Sources/SwiftSQLExt/SwiftSQLExt.swift @@ -70,9 +70,9 @@ public struct SQLRow { /// column index is out of range, the result is undefined. /// /// - parameter index: The leftmost column of the result set has the index 0. - public subscript(index: Int) -> T { - let value = values[index]! - guard let convertedValue = T.convert(from: value) else { + public subscript(index: Int) -> T { + let value = values[index] + guard let convertedValue = T(sqlColumnValue: value) else { fatalError("Could not convert \(type(of: value)). Make sure target type (\(T.self)) correctly implements convert(from:).") } return convertedValue @@ -85,9 +85,8 @@ public struct SQLRow { /// column index is out of range, the result is undefined. /// /// - parameter index: The leftmost column of the result set has the index 0. - public subscript(index: Int) -> T? { - guard let value = values[index] else { return nil } - return T.convert(from: value) + public subscript(index: Int) -> T? { + return T(sqlColumnValue: values[index]) } /// Returns a single column (by its name) of the current result row of a query. @@ -96,7 +95,7 @@ public struct SQLRow { /// If the passed columnName doesn't point to a valid column name, a fatal error is raised. /// /// - parameter columnName: The name of the column. - public subscript(columnName: String) -> T { + public subscript(columnName: String) -> T { guard let columnIndex = columnIndicesByNames[columnName] else { fatalError("No such column \(columnName)") } @@ -109,17 +108,21 @@ public struct SQLRow { /// If the passed columnName doesn't point to a valid column name, nil is returned. /// /// - parameter columnName: The name of the column. - public subscript(columnName: String) -> T? { + public subscript(columnName: String) -> T? { guard let columnIndex = columnIndicesByNames[columnName] else { return nil } return self[columnIndex] } - private let values: [Any?] + private let values: [SQLColumnValue] private let columnIndicesByNames: [String : Int] } public protocol SQLRowDecodable { init(row: SQLRow) throws } + +public protocol InitializableBySQLColumnValue { + init?(sqlColumnValue: SQLColumnValue) +} diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift index 633bde8..fa78339 100644 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift @@ -113,7 +113,56 @@ final class SwiftSQLExtTests: XCTestCase { XCTAssertNil(try XCTUnwrap(row)["Level"] as Int?) } + func testInitializableBySQLColumnValueConveniencesIn64Source() throws { + // GIVEN + let sqlColumnValue = SQLColumnValue.int64(1) + + // WHEN + let int32 = Int32(sqlColumnValue: sqlColumnValue) + let int64 = Int64(sqlColumnValue: sqlColumnValue) + let double = Double(sqlColumnValue: sqlColumnValue) + let string = String(sqlColumnValue: sqlColumnValue) + + //THEN + XCTAssertEqual(try XCTUnwrap(int32), 1) + XCTAssertEqual(try XCTUnwrap(int64), 1) + XCTAssertEqual(try XCTUnwrap(double), 1) + XCTAssertEqual(try XCTUnwrap(string), "1") + } + + func testInitializableBySQLColumnValueConveniencesDoubleSource() throws { + // GIVEN + let sqlColumnValue = SQLColumnValue.double(1) + + // WHEN + let int = Int(sqlColumnValue: sqlColumnValue) + let int32 = Int32(sqlColumnValue: sqlColumnValue) + let int64 = Int64(sqlColumnValue: sqlColumnValue) + let string = String(sqlColumnValue: sqlColumnValue) + + //THEN + XCTAssertEqual(try XCTUnwrap(int), 1) + XCTAssertEqual(try XCTUnwrap(int32), 1) + XCTAssertEqual(try XCTUnwrap(int64), 1) + XCTAssertEqual(try XCTUnwrap(string), "1.0") + } + func testInitializableBySQLColumnValueConveniencesStringSource() throws { + // GIVEN + let sqlColumnValue = SQLColumnValue.string("1") + + // WHEN + let int = Int(sqlColumnValue: sqlColumnValue) + let int32 = Int32(sqlColumnValue: sqlColumnValue) + let int64 = Int64(sqlColumnValue: sqlColumnValue) + let double = Double(sqlColumnValue: sqlColumnValue) + + //THEN + XCTAssertEqual(try XCTUnwrap(int), 1) + XCTAssertEqual(try XCTUnwrap(int32), 1) + XCTAssertEqual(try XCTUnwrap(int64), 1) + XCTAssertEqual(try XCTUnwrap(double), 1) + } } private extension SQLConnection { From 4b3d553b4441c87ba913d940beac656dbe347865 Mon Sep 17 00:00:00 2001 From: Ahmed Khalaf Date: Thu, 11 Jun 2020 17:28:47 +0200 Subject: [PATCH 8/8] case-insensitive column names for SQLStatement and SQLRow --- Sources/SwiftSQL/SQLStatement.swift | 9 ++++++--- Sources/SwiftSQLExt/SwiftSQLExt.swift | 8 +++----- Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift | 13 +++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftSQL/SQLStatement.swift b/Sources/SwiftSQL/SQLStatement.swift index f935d6e..9c170ff 100644 --- a/Sources/SwiftSQL/SQLStatement.swift +++ b/Sources/SwiftSQL/SQLStatement.swift @@ -269,13 +269,16 @@ public final class SQLStatement { /// Returns the index of a column given its name. public func columnIndex(forName name: String) -> Int? { - return columnIndices[name] + return columnIndices[name.lowercased()] } - private lazy var columnIndices: [String : Int] = { + + /// Holds each column index key-ed by its name. + /// Initialized for all columns as soon as it's first accessed. + public private(set) lazy var columnIndices: [String : Int] = { var indices: [String : Int] = [:] indices.reserveCapacity(columnCount) for index in 0..(columnName: String) -> T { - guard let columnIndex = columnIndicesByNames[columnName] else { + guard let columnIndex = columnIndicesByNames[columnName.lowercased()] else { fatalError("No such column \(columnName)") } return self[columnIndex] @@ -109,7 +107,7 @@ public struct SQLRow { /// /// - parameter columnName: The name of the column. public subscript(columnName: String) -> T? { - guard let columnIndex = columnIndicesByNames[columnName] else { + guard let columnIndex = columnIndicesByNames[columnName.lowercased()] else { return nil } return self[columnIndex] diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift index fa78339..6857fb7 100644 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift @@ -163,6 +163,19 @@ final class SwiftSQLExtTests: XCTestCase { XCTAssertEqual(try XCTUnwrap(int64), 1) XCTAssertEqual(try XCTUnwrap(double), 1) } + + func testCaseInsensitiveColumnNameSubscripts() throws { + // GIVEN + try db.populateStore() + + // WHEN + let row = try db + .prepare("SELECT Name FROM Persons ORDER BY Level ASC") + .row() + + // THEN + XCTAssertEqual(try XCTUnwrap(row)["name"] as String, "Alice") + } } private extension SQLConnection {