diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSQL-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSQL-Package.xcscheme index 23690e5..7a5df87 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSQL-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/SwiftSQL-Package.xcscheme @@ -1,6 +1,6 @@ + + + +

-**SwiftSQL** is a micro Swift [SQLite](https://www.sqlite.org/index.html) wrapper, solid and meticulously documented. It maps directly to the SQLite concepts and doesn't introduce anything beyond them. +**SwiftSQL** is a micro Swift [SQLite](https://www.sqlite.org/index.html) wrapper, solid and meticulously documented. It maps directly to the SQLite concepts and doesn't introduce anything beyond them. The entire library fits just under 300 lines of code, but gets you 80% there. **SwiftSQLExt** introduces some basic conveniences on top of it. -SwiftSQL was created for [Pulse](https://github.com/kean/Pulse) where it is embedded internally. The entire library fits just under 300 lines of code, but gets you 80% there. -
# Usage diff --git a/Sources/SwiftSQL/SQLConnection.swift b/Sources/SwiftSQL/SQLConnection.swift index 0e70ff4..4ffafdf 100644 --- a/Sources/SwiftSQL/SQLConnection.swift +++ b/Sources/SwiftSQL/SQLConnection.swift @@ -14,7 +14,8 @@ import SQLite3 /// For more details about using multiple database connections to improve concurrency, please refer to the /// [documentation](https://www.sqlite.org/isolation.html). public final class SQLConnection { - private(set) var ref: OpaquePointer! + public private(set) var ref: OpaquePointer! + let _hook: Hook // = .init() jmj /// Returns the last [INSERT row id](https://www.sqlite.org/c3ref/last_insert_rowid.html) /// of the database connection. Returns `0` if no successfull INSERT into rowid @@ -51,10 +52,17 @@ public final class SQLConnection { /// /// - note: See [SQLite: Result and Error Codes](https://www.sqlite.org/rescode.html) /// for more information. - public init(location: Location, mode: Mode = .writable(create: true), options: Options = Options()) throws { + public init( + location: Location, + mode: Mode = .writable(create: true), + options: Options = Options(), + hook: Hook = .default + ) throws { let path: String var flags: Int32 = 0 + _hook = hook + switch mode { case .readonly: flags |= SQLITE_OPEN_READONLY @@ -90,12 +98,24 @@ public final class SQLConnection { } try isOK(sqlite3_open_v2(path, &ref, flags, nil)) + + _hook.registerHandlers(self) } deinit { try? close() } + // MARK: + /// [Executes](https://www.sqlite.org/c3ref/changes.html) to return the number of rows changed by the most recent statement. + public var lastChangeCount: Int64 { + if #available(macOS 12.3, *) { + sqlite3_changes64(ref) + } else { + Int64(sqlite3_changes(ref)) + } + } + // MARK: Execute /// [Executes](https://www.sqlite.org/c3ref/exec.html) the given one-shot SQL statement. diff --git a/Sources/SwiftSQL/SQLDataType.swift b/Sources/SwiftSQL/SQLDataType.swift deleted file mode 100644 index d033765..0000000 --- a/Sources/SwiftSQL/SQLDataType.swift +++ /dev/null @@ -1,82 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2020 Alexander Grebenyuk (github.com/kean). - -import Foundation -import SQLite3 - -/// Represents a data type supported by SQLite. -/// -/// - note: To add support for custom data types, like `Bool` or `Date`, see -/// [Advanced Usage Guide](https://github.com/kean/SwiftSQL/blob/0.1.0/Docs/advanced-usage-guide.md) -public protocol SQLDataType { - func sqlBind(statement: OpaquePointer, index: Int32) - static func sqlColumn(statement: OpaquePointer, index: Int32) -> Self -} - -extension Int: SQLDataType { - public func sqlBind(statement: OpaquePointer, index: Int32) { - sqlite3_bind_int64(statement, index, Int64(self)) - } - - public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int { - Int(sqlite3_column_int64(statement, index)) - } -} - -extension Int32: SQLDataType { - public func sqlBind(statement: OpaquePointer, index: Int32) { - sqlite3_bind_int(statement, index, self) - } - - public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int32 { - sqlite3_column_int(statement, index) - } -} - -extension Int64: SQLDataType { - public func sqlBind(statement: OpaquePointer, index: Int32) { - sqlite3_bind_int64(statement, index, self) - } - - public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Int64 { - sqlite3_column_int64(statement, index) - } -} - -extension Double: SQLDataType { - public func sqlBind(statement: OpaquePointer, index: Int32) { - sqlite3_bind_double(statement, index, self) - } - - public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Double { - sqlite3_column_double(statement, index) - } -} - -extension String: SQLDataType { - public func sqlBind(statement: OpaquePointer, index: Int32) { - sqlite3_bind_text(statement, index, self, -1, SQLITE_TRANSIENT) - } - - public static func sqlColumn(statement: OpaquePointer, index: Int32) -> String { - guard let pointer = sqlite3_column_text(statement, index) else { return "" } - return String(cString: pointer) - } -} - -extension Data: SQLDataType { - public func sqlBind(statement: OpaquePointer, index: Int32) { - sqlite3_bind_blob(statement, Int32(index), Array(self), Int32(count), SQLITE_TRANSIENT) - } - - public static func sqlColumn(statement: OpaquePointer, index: Int32) -> Data { - guard let pointer = sqlite3_column_blob(statement, Int32(index)) else { - return Data() - } - let count = Int(sqlite3_column_bytes(statement, Int32(index))) - return Data(bytes: pointer, count: count) - } -} - -private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) diff --git a/Sources/SwiftSQL/SQLHook.swift b/Sources/SwiftSQL/SQLHook.swift new file mode 100644 index 0000000..1c1e005 --- /dev/null +++ b/Sources/SwiftSQL/SQLHook.swift @@ -0,0 +1,163 @@ +import Foundation +import Combine +import SQLite3 +//import SwiftSQL + +// MARK: - SQLite hooks + +public extension SQLConnection { + + func publisher() -> AnyPublisher { + _hook.publisher() + } + + struct UpdateInfo: CustomStringConvertible { + public let database: String + public let tableName: String + public let op_code: Int32 + public let rowid: Int64 + + init(op: Int32, + db: UnsafePointer?, + table: UnsafePointer?,row: Int64) + { + self.op_code = op + self.rowid = row + + if let name = db { + self.database = String(cString: name) + } + else { self.database = "main" } + + if let name = table { + self.tableName = String(cString: name) + } + else { self.tableName = "" } + } + + public var description: String { + "\(database):\(tableName) \(op)(\(rowid))" + } + + var isInsert: Bool { op_code == SQLITE_INSERT } + var isUpdate: Bool { op_code == SQLITE_UPDATE } + var isDelete: Bool { op_code == SQLITE_DELETE } + + var op: String { + switch op_code { + case SQLITE_UPDATE: return "update" + case SQLITE_DELETE: return "delete" + case SQLITE_INSERT: return "insert" + default: + return "" + } + } + } + + func createUpdateHandler(_ block: @escaping (UpdateInfo) -> Void) { + + let updateBlock: UpdateHookCallback = { _, op, dbName, tableName, rowid in + guard let tableName = tableName else { return } + let info = UpdateInfo(op: op, db: dbName, table: tableName, row: rowid) + block(info) +// block(String(cString: tableName)) + } + + _hook.update = updateBlock + let hookAsContext = Unmanaged.passUnretained(_hook).toOpaque() + sqlite3_update_hook(ref, updateHookWrapper, hookAsContext) + } + + func removeUpdateHandler() { + sqlite3_update_hook(ref, nil, nil) + _hook.update = nil + } + + func createCommitHandler(_ block: @escaping () -> Void) { + _hook.commit = block + let hookAsContext = Unmanaged.passUnretained(_hook).toOpaque() + sqlite3_commit_hook(ref, commitHookWrapper, hookAsContext) + } + + func removeCommitHandler() { + sqlite3_commit_hook(ref, nil, nil) + _hook.commit = nil + } + + func createRollbackHandler(_ block: @escaping () -> Void) { + _hook.rollback = block + let hookAsContext = Unmanaged.passUnretained(_hook).toOpaque() + sqlite3_rollback_hook(ref, rollbackHookWrapper, hookAsContext) + } + + func removeRollbackHandler() { + sqlite3_rollback_hook(ref, nil, nil) + _hook.rollback = nil + } +} + + +// MARK: - Hook Interface +typealias UpdateHookCallback = + (UnsafeMutableRawPointer?, Int32, UnsafePointer?, UnsafePointer?, Int64) -> Void + + +import SwiftUI +import Combine + +public class Hook { + public enum Event { case didRollback, didCommit, didUpdate(SQLConnection.UpdateInfo) } + + var update: UpdateHookCallback? + var commit: (() -> Void)? + var rollback: (() -> Void)? + var _publisher: PassthroughSubject = .init() + + public init() { + } + + func publisher() -> AnyPublisher { + _publisher.eraseToAnyPublisher() + } + + func registerHandlers(_ db: SQLConnection) { + db.createCommitHandler { [weak self] in + self?._publisher.send(.didCommit) + } + db.createRollbackHandler { [weak self] in + self?._publisher.send(.didRollback) + } + db.createUpdateHandler { [weak self] in + self?._publisher.send(.didUpdate($0)) + } + } +} + +public extension Hook { + static var `default`: Hook = Hook() +} + +func updateHookWrapper( + context: UnsafeMutableRawPointer?, + operationType: Int32, + databaseName: UnsafePointer?, + tableName: UnsafePointer?, + rowid: sqlite3_int64 +) -> Void { + guard let context = context else { return } + let hook = Unmanaged.fromOpaque(context).takeUnretainedValue() + hook.update?(context, operationType, databaseName, tableName, rowid) +} + +func commitHookWrapper(context: UnsafeMutableRawPointer?) -> Int32 { + guard let context = context else { return 0 } + let hook = Unmanaged.fromOpaque(context).takeUnretainedValue() + hook.commit?() + return 0 +} + +func rollbackHookWrapper(context: UnsafeMutableRawPointer?) { + guard let context = context else { return } + let hook = Unmanaged.fromOpaque(context).takeUnretainedValue() + hook.rollback?() +} diff --git a/Sources/SwiftSQL/SQLStatement.swift b/Sources/SwiftSQL/SQLStatement.swift index 62397db..f741901 100644 --- a/Sources/SwiftSQL/SQLStatement.swift +++ b/Sources/SwiftSQL/SQLStatement.swift @@ -24,7 +24,7 @@ import SQLite3 /// """) /// /// 2. Bind values to parameters using one of the `bind()` methods. The provided -/// values must be one of the data types supported by SQLite (see `SQLDataType` for +/// values must be one of the data types supported by SQLite (see `SQLBindable` for /// more info) /// /// try statement.bind("Alexander", "Grebenyuk") @@ -48,8 +48,8 @@ import SQLite3 /// `SQLStatement` object gets deallocated. public final class SQLStatement { let db: SQLConnection - let ref: OpaquePointer - + public let ref: OpaquePointer + init(db: SQLConnection, ref: OpaquePointer) { self.db = db self.ref = ref @@ -84,90 +84,40 @@ public final class SQLStatement { /// /// - note: See [SQLite: Result and Error Codes](https://www.sqlite.org/rescode.html) /// for more information. - public func execute() throws { - try isOK(sqlite3_step(ref)) - } - - // MARK: Binding Parameters - - /// Binds values to the statement parameters. - /// - /// try db.prepare("INSERT INTO Users (Level, Name) VALUES (?, ?)") - /// .bind(80, "John") - /// .execute() - /// @discardableResult - public func bind(_ parameters: SQLDataType?...) throws -> Self { - try bind(parameters) + public func execute() throws -> SQLStatement { + try isOK(sqlite3_step(ref)) return self } + + public func bind(value: Any?, at index: Int) throws { + let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) - /// Binds values to the statement parameters. - /// - /// try db.prepare("INSERT INTO Users (Level, Name) VALUES (?, ?)") - /// .bind([80, "John"]) - /// .execute() - /// - @discardableResult - public func bind(_ parameters: [SQLDataType?]) throws -> Self { - for (index, value) in zip(parameters.indices, parameters) { - try _bind(value, at: Int32(index + 1)) + let index = Int32(index) + if value == nil { + sqlite3_bind_null(ref, index) } - return self - } - - /// Binds values to the named statement parameters. - /// - /// let row = try db.prepare("SELECT Level, Name FROM Users WHERE Name = :param LIMIT 1") - /// .bind([":param": "John""]) - /// .next() - /// - /// - parameter name: The name of the parameter. If the name is missing, throws - /// an error. - @discardableResult - public func bind(_ parameters: [String: SQLDataType?]) throws -> Self { - for (key, value) in parameters { - try _bind(value, for: key) + else if let value = value as? Bool { + sqlite3_bind_int64(ref, index, value ? 1 : 0) + } else if let value = value as? Data { + sqlite3_bind_blob(ref, index, Array(value), Int32(value.count), SQLITE_TRANSIENT) } - return self - } - - /// Binds values to the parameter with the given name. - /// - /// let row = try db.prepare("SELECT Level, Name FROM Users WHERE Name = :param LIMIT 1") - /// .bind("John", for: ":param") - /// .next() - /// - /// - parameter name: The name of the parameter. If the name is missing, throws - /// an error. - @discardableResult - public func bind(_ value: SQLDataType?, for name: String) throws -> Self { - try _bind(value, for: name) - return self - } - - /// Binds value to the given index. - /// - /// - parameter index: The index starts at 0. - @discardableResult - public func bind(_ value: SQLDataType?, at index: Int) throws -> Self { - try _bind(value, at: Int32(index + 1)) - return self - } - - private func _bind(_ value: SQLDataType?, for name: String) throws { - let index = sqlite3_bind_parameter_index(ref, name) - guard index > 0 else { - throw SQLError(code: SQLITE_MISUSE, message: "Failed to find parameter named \(name)") + else if let value = value as? (any FixedWidthInteger) { + sqlite3_bind_int64(ref, index, Int64(value)) } - try _bind(value, at: index) - } - - private func _bind(_ value: SQLDataType?, at index: Int32) throws { - if let value = value { - value.sqlBind(statement: ref, index: index) - } else { - sqlite3_bind_null(ref, index) + else if let value = value as? (any BinaryFloatingPoint) { + sqlite3_bind_double(ref, index, Double(value)) + } + else if let value = value as? String { + sqlite3_bind_text(ref, index, value, -1, SQLITE_TRANSIENT) + } + else if let value = value as? (any StringProtocol) { + sqlite3_bind_text(ref, index, String(value), -1, SQLITE_TRANSIENT) + } + else { + throw SQLError( + code: #line, + message: "Cannot bind \(String(describing: value)) of type \(type(of: value))") } } @@ -196,7 +146,7 @@ public final class SQLStatement { Int(sqlite3_bind_parameter_count(ref)) } - // MARK: Accessing Columns + // MARK: Accessing Column Values /// Returns a single column of the current result row of a query. /// @@ -204,22 +154,146 @@ public final class SQLStatement { /// column index is out of range, the result is undefined. /// /// - parameter index: The leftmost column of the result set has the index 0. - public func column(at index: Int) -> T { - T.sqlColumn(statement: ref, index: Int32(index)) + + // MARK: - Builtin Column Value Types + // SQLITE_TEXT + public func column(at ndx: Int) -> String { + column(at: ndx) ?? "" } - /// Returns a single column of the current result row of a query. If the - /// value is `Null`, returns `nil.` - /// - /// If the SQL statement does not currently point to a valid row, or if the - /// column index is out of range, the result is undefined. - /// - /// - parameter index: The leftmost column of the result set has the index 0. - public func column(at index: Int) -> T? { - if sqlite3_column_type(ref, Int32(index)) == SQLITE_NULL { - return nil + public func column(at ndx: Int) -> String? { + sqlite3_column_type(ref, Int32(ndx)) == SQLITE_TEXT + ? String(cString: sqlite3_column_text(ref, Int32(ndx))) + : nil + } + + // SQLITE_INTEGER + public func column( + at ndx: Int, + as vtype: V.Type = V.self) + -> V { + column(at: ndx) ?? .zero + } + + public func column( + at ndx: Int, + as vtype: V.Type = V.self) + -> V? { + sqlite3_column_type(ref, Int32(ndx)) == SQLITE_INTEGER + ? V(sqlite3_column_int64(ref, Int32(ndx))) + : nil + } + + // SQLITE_FLOAT + public func column( + at ndx: Int, + as v: V.Type = V.self) + -> V { + column(at: ndx) ?? .zero + } + + public func column( + at ndx: Int, + as v: V.Type = V.self) + -> V? { + sqlite3_column_type(ref, Int32(ndx)) == SQLITE_FLOAT + ? V(sqlite3_column_double(ref, Int32(ndx))) + : nil + } + + // SQLITE_BLOB + public func column(at index: Int) -> Data { + column(at: index) ?? Data() + } + + public func column(at index: Int) -> Data? { + let ndx = Int32(index) + guard sqlite3_column_type(ref, ndx) == SQLITE_BLOB + else { return nil } + if let bytes = sqlite3_column_blob(ref, ndx) { + let byteCount = sqlite3_column_bytes(ref, ndx) + return Data(bytes: bytes, count: Int(byteCount)) } else { - return T.sqlColumn(statement: ref, index: Int32(index)) + return nil + } + } + + public var dictionaryValue: [String: Any] { + var dict: [String:Any] = .init() + for n in columnNames { + let value = self.anyValue(at: columnIndex(forName: n)!) + dict[n] = value + } + return dict + } + + public var keyValuePairs: [(key: String, value: Any)] { + var dict: [(key: String, value: Any)] = .init() + for n in columnNames { + if let value = self.anyValue(at: columnIndex(forName: n)!) { + dict.append((n, value)) + } else { + dict.append((n, "nil")) + } + } + return dict + } + + public func value(named: String, as vtype: Any.Type = Any.self) throws -> Any { + guard let ndx = columnIndex(forName: named) + else { throw SQLError(code: #line, message: "No column named '\(named)'") } + return try value(at: ndx, as: vtype) + } + + public func value(at ndx: Int, as vtype: Any.Type = Any.self) throws -> Any { + var value = anyValue(at: ndx) as Any + if type(of: value) == vtype { return value } + if let opt = value as? OptionalProtocol { + value = opt.honestValue ?? value + } + if let value = value as? (any FixedWidthInteger), + vtype is Bool.Type { + let ival = Int(value) + return (ival != 0) + } + if let value = value as? (any FixedWidthInteger), + let fn = vtype as? any FixedWidthInteger.Type { + return fn.init(value) + } + if let value = value as? (any BinaryFloatingPoint), + let fn = vtype as? any BinaryFloatingPoint.Type { + return fn.init(value) + } + if type(of: value) == vtype { return value } + + throw SQLError(code: #line, + message: "Error reading \(value) column at '\(ndx)'") + } + + func anyValue(at index: Int) -> Any? { + + let index = Int32(index) + let type = sqlite3_column_type(ref, index) + // switch (type, V.self) { + // case (SQLITE_INTEGER, is Int64.Type): + 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() + } + case SQLITE_NULL: + return nil + default: + return nil } } @@ -241,6 +315,35 @@ 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.lowercased()] + } + + /// 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.. Self +} + +extension Array: ArrayProtocol { + public static var elementType: Any.Type { + Element.self + } + public static func empty() -> Array { + Self() + } +} + +// MARK: Storable +public typealias StorableRepresentation = Storable + +public protocol Storable { + var storableRepresentation: StorableRepresentation { get } +} + +extension Optional: Storable where Wrapped: Storable { + public var storableRepresentation: StorableRepresentation { + switch self { + case .none: return self + case .some(let s): return s.storableRepresentation + } + } +} + +public protocol BuiltinStorable: Storable { + var builtinRepresentation: Any { get } +} + +extension BuiltinStorable { + + public var builtinRepresentation: Any { self } + public var storableRepresentation: StorableRepresentation { + self + } +} + +// TODO: Add Date and JSON as Storables +extension Date: Storable { + + public var storableRepresentation: StorableRepresentation { + let bitPattern = self.timeIntervalSinceReferenceDate.bitPattern + return String(bitPattern) // .builtinRepresentation as! StorableRepresentation + } +} + +// TODO: Move to SQLStatement +import SQLite3 + +extension Data: BuiltinStorable {} +extension String: BuiltinStorable {} +extension Bool: BuiltinStorable {} + +// FixedWidthInteger +extension Int: BuiltinStorable {} +extension Int8: BuiltinStorable {} +extension Int16: BuiltinStorable {} +extension Int32: BuiltinStorable {} +extension Int64: BuiltinStorable {} + +// BinaryFloatingPoint +extension CGFloat: BuiltinStorable {} +extension Float16: BuiltinStorable {} +extension Float32: BuiltinStorable {} +extension Float64: BuiltinStorable {} + +extension SQLStatement { + + @_disfavoredOverload + public func value( + at index: Int, + as t: T.Type = T.self) + throws -> T? { + self.anyValue(at: index) as? T + } + + public func value(at ndx: Int, as t: T.Type = T.self) + throws -> T + where T: FixedWidthInteger + { + let v = self.anyValue(at: ndx) + if let v, let r = v as? T { return r } + if let v = v as? (any FixedWidthInteger) { + return T(v) + } + throw SQLError(code: #line, + message: "Unable to cast \(String(describing: v)) at \(ndx) to \(t)") + } + + public func value(at ndx: Int, as t: T.Type = T.self) + throws -> T { + let v = self.anyValue(at: ndx) + if let v, let r = v as? T { return r } + throw SQLError(code: #line, + message: "Unable to cast \(String(describing: v)) at \(ndx) to \(t)") + } + + @discardableResult + public func bind(_ parameters: Storable?...) throws -> Self { + try bind(parameters) + return self + } + + @discardableResult + public func bind(_ parameters: [Storable?]) throws -> Self { + for (index, value) in zip(parameters.indices, parameters) { + try bind(value: value, at: Int(index + 1)) + } + return self + } + /// Binds values to the named statement parameters. + /// + /// let row = try db.prepare("SELECT Level, Name FROM Users WHERE Name = :param LIMIT 1") + /// .bind([":param": "John""]) + /// .next() + /// + /// - parameter name: The name of the parameter. If the name is missing, throws + /// an error. + @discardableResult + public func bind(_ parameters: [String: Storable?]) throws -> Self { + for (key, value) in parameters { + try bind(value, for: key) + } + return self + } + + @discardableResult + public func bind(_ value: Storable?, for name: String) throws -> Self { + let ndx = sqlite3_bind_parameter_index(ref, name) + guard ndx > 0 else { + throw SQLError(code: SQLITE_MISUSE, message: "Failed to find parameter named \(name)") + } + return try bind(value, at: Int(ndx-1)) + } + + // MARK: - Bind + @_disfavoredOverload + public func bind(_ value: Any?, at index: Int) throws -> Self { + guard let value else { + sqlite3_bind_null(ref, Int32(index)) + return self + } + if let builtin = value as? Storable { + return try bind(builtin, at: index) + } + // else + throw SQLError(code: #line, message: "Unsupport type \(type(of: value))") + } + + public func bind(_ value: Storable?, at index: Int) throws -> Self { + guard let value else { + // The leftmost SQL parameter has an index of 1. + sqlite3_bind_null(ref, Int32(index+1)) + return self + } + if let builtin = value as? BuiltinStorable { + try bind(value: builtin, at: index) + } else { + try bind(value: value.storableRepresentation, at: index) + } + return self + } + + /* + Binding Values To Prepared Statements + https://www.sqlite.org/c3ref/bind_blob.html + + The second argument is the index of the SQL parameter to be set. The leftmost SQL + parameter has an index of 1. When the same named SQL parameter is used more than + once, second and subsequent occurrences have the same index as the first occurrence. + The index for named parameters can be looked up using the sqlite3_bind_parameter_index() + API if desired. The index for "?NNN" parameters is the value of NNN. The NNN value + must be between 1 and the sqlite3_limit() parameter SQLITE_LIMIT_VARIABLE_NUMBER + (default value: 32766). + */ + func bind(value: BuiltinStorable?, at index: Int) throws { +// guard index > 0 else { +// throw SQLError(code: #line, message: "Bind index \(index) is out of bounds") +// } + let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + + let index = Int32(index+1) + if value == nil { + sqlite3_bind_null(ref, index) + } + else if let value = value as? Data { + sqlite3_bind_blob(ref, index, Array(value), Int32(value.count), SQLITE_TRANSIENT) + } + else if let value = value as? (any FixedWidthInteger) { + sqlite3_bind_int64(ref, index, Int64(value)) + } + else if let value = value as? (any BinaryFloatingPoint) { + sqlite3_bind_double(ref, index, Double(value)) + } + else if let value = value as? String { + sqlite3_bind_text(ref, index, value, -1, SQLITE_TRANSIENT) + } + else if let value = value as? (any StringProtocol) { + sqlite3_bind_text(ref, index, String(value), -1, SQLITE_TRANSIENT) + } + else { + throw SQLError(code: #line, message: "Cannot bind value of type \(type(of: value))") + } + } + +} diff --git a/Sources/SwiftSQLExt/ArrayBuilder.swift b/Sources/SwiftSQLExt/ArrayBuilder.swift new file mode 100644 index 0000000..3822368 --- /dev/null +++ b/Sources/SwiftSQLExt/ArrayBuilder.swift @@ -0,0 +1,95 @@ +/// Allows creation of SwiftUI-style arrays using a closure. Watch this +/// [WWDC session](https://developer.apple.com/videos/play/wwdc2021/10253/) showing how it works. +/// +/// ``` +/// Array { +/// "1" +/// "2" +/// let trueCondition = true || Bool.random() +/// if trueCondition { +/// "maybe 3" +/// } +/// let falseCondition = false && Bool.random() +/// if falseCondition { +/// "maybe 4" +/// } +/// for i in (5...7) { +/// "loop \(i)" +/// } +/// Optional.none +/// Optional.some("unwrapped 8") +/// } +/// ``` +/// Result: +/// ``` +/// ["1", "2", "maybe 3", "loop 5", "loop 6", "loop 7", "unwrapped 8"] +/// ``` +@resultBuilder +public enum ArrayBuilder { + public typealias Expression = Element + public typealias Component = [Element] + + // General + public static func buildPartialBlock(first: Never) -> Component { } + public static func buildBlock() -> [Element] { + [] + } + public static func buildExpression(_ expression: Expression) -> Component { + [expression] + } + public static func buildExpression(_ expression: [Expression]) -> Component { + expression + } + public static func buildBlock(_ component: Component) -> Component { + component + } + + // Optionals + public static func buildOptional(_ children: Component?) -> Component { + children ?? [] + } + public static func buildExpression(_ expression: Expression?) -> Component { + expression.map { [$0] } ?? [] + } + + // Logic + public static func buildIf(_ element: Component?) -> Component { + element ?? [] + } + public static func buildEither(first child: Component) -> Component { + child + } + public static func buildEither(second child: Component) -> Component { + child + } + public static func buildPartialBlock(first: Void) -> Component { + [] + } + public static func buildPartialBlock(first: Expression) -> Component { + [first] + } + public static func buildPartialBlock(first: Component) -> Component { + first + } + + + // Loops + public static func buildArray(_ components: [Component]) -> Component { + components.flatMap { $0 } + } + public static func buildBlock(_ children: Component...) -> Component { + children.flatMap { $0 } + } + public static func buildPartialBlock(accumulated: Component, next: Expression) -> Component { + accumulated + [next] + } + public static func buildPartialBlock(accumulated: Component, next: Component) -> Component { + accumulated + next + } +} + +//public extension Array { +// init(@ArrayBuilder makeItems: Factory<[Element]>) { +// self.init(makeItems()) +// } +//} diff --git a/Sources/SwiftSQLExt/PreciseDateFormatter.swift b/Sources/SwiftSQLExt/PreciseDateFormatter.swift new file mode 100644 index 0000000..db1db4b --- /dev/null +++ b/Sources/SwiftSQLExt/PreciseDateFormatter.swift @@ -0,0 +1,75 @@ +import Foundation +import SwiftSQL + +//extension Date: SQLBindable { +// public static var defaultSQLBinder: SQLBinder { +// let sb = String.defaultSQLBinder +// return .init( +// getf: { +// PreciseDateFormatter.date(from: sb.get(from: $0, at: $1))! +// }, +// setf: { +// sb.set(from: $0, at: $1, to: PreciseDateFormatter.string(from: $2)) +// }) +// } +//} +// +//public extension AnySQLBinder { +// static var created_at: Self { Date.defaultSQLBinder.named("created_at") as! Self } +// static var updated_at: Self { Date.defaultSQLBinder.named("updated_at") as! Self } +//} + +// `ISO8601DateFormatter` does not maintain nanosecond precision, which makes it +// nearly impossible to equate encodable objects that include `Date` properties. +// `DateFormatter` maintains nanosecond precision by storing the exact +// bit pattern of `Date.timeIntervalSinceReferenceDate`, which is the type's +// underlying primitive. https://developer.apple.com/documentation/foundation/nsdate +public struct PreciseDateFormatter { + public static func string(from date: Date) -> String { + let bitPattern = date.timeIntervalSinceReferenceDate.bitPattern + return String(bitPattern) + } + + public static func date(from string: String) -> Date? { + guard let bitPattern = UInt64(string) else { return nil } + let double = Double(bitPattern: bitPattern) + return Date(timeIntervalSinceReferenceDate: double) + } +} + +extension KeyedDecodingContainer { + public func decodePreciseDate(forKey key: K) throws -> Date { + let asString = try self.decode(String.self, forKey: key) + guard let date = PreciseDateFormatter.date(from: asString) else { + let context = DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Could not parse '\(asString)' into Date." + ) + throw Swift.DecodingError.typeMismatch(Date.self, context) + } + return date + } + + public func decodePreciseDateIfPresent(forKey key: K) throws -> Date? { + guard let asString = try self.decodeIfPresent(String.self, forKey: key) else { return nil } + guard let date = PreciseDateFormatter.date(from: asString) else { + let context = DecodingError.Context( + codingPath: self.codingPath, + debugDescription: "Could not parse '\(asString)' into Date." + ) + throw Swift.DecodingError.typeMismatch(Date.self, context) + } + return date + } +} + +extension KeyedEncodingContainer { + public mutating func encode(preciseDate: Date, forKey key: K) throws { + try self.encode(PreciseDateFormatter.string(from: preciseDate), forKey: key) + } + + public mutating func encodeIfPresent(preciseDate: Date?, forKey key: K) throws { + guard let preciseDate = preciseDate else { return } + try self.encodeIfPresent(PreciseDateFormatter.string(from: preciseDate), forKey: key) + } +} diff --git a/Sources/SwiftSQLExt/Schema.swift b/Sources/SwiftSQLExt/Schema.swift new file mode 100644 index 0000000..b0d92aa --- /dev/null +++ b/Sources/SwiftSQLExt/Schema.swift @@ -0,0 +1,352 @@ +// +// Schema.swift +// +// +// Created by Jason Jobe on 2/2/23. +// + +import Foundation +import KeyValueCoding +import SwiftSQL + +public protocol ExpressibleByDefault { + init(defaultContext: ()) +} + +public extension ExpressibleByDefault { + static func defaultValue() -> Self { + .init(defaultContext: ()) + } +} + +public struct Schema { + public typealias ID = Int64 + public private(set) var table: String + public private(set) var valueType: E.Type + var md: Metadata { swift_metadata(of: valueType) } +} + +public extension Schema { + init(table: String? = nil, for t: E.Type) { + self.valueType = t + self.table = table ?? String(describing: t) + } +} + +public extension Schema { + func instantiate( + _ type: T.Type = T.self, + from stm: SQLStatement, + strict: Bool = true + ) throws -> T { + guard valueType is T.Type else { + throw SQLError(code: #line, message: "Cannot instantiate \(type)") + } + var it: T = .defaultValue() + + for p in md.properties { + var v: Any? +// if let s = v as? Storable { +// v = s.storableRepresentation +// } + if strict { + v = try stm.value(named: p.name, as: p.metadata.type) + } else { + v = try? stm.value(named: p.name, as: p.metadata.type) + } + swift_setValue(v, to: &it, key: p.name) + } + return it + } +} + +// MARK: - SQLite Related Extensions +public extension Schema { + func create(in db: SQLConnection, table: String) throws { + let sql = sql(create: table) + try db.execute(sql) + } + + func insert(in db: SQLConnection, _ rows: [E]) throws { + let insert = try db.prepare(sql(insert: table)) + for row in rows { + try insert.rebind(row).execute() + } + } + + func select( + in db: SQLConnection, + where cond: String? = nil, + limit: Int = 0, + fn: (E) -> Void) + throws { + let select = try db.prepare(sql(select: table, where: cond, limit: limit)) + while try select.step() { + let t: E = try instantiate(from: select, strict: false) + fn(t) + } + } +} + +public extension Schema { + + /* + INSERT INTO table (column1,column2 ,..) + VALUES( value1, value2 ,...); + + INSERT OR REPLACE INTO table(column_list) + VALUES(value_list); + + REPLACE INTO table(column_list) + VALUES(value_list); + + UPDATE test SET (foo, bar) = ( 8, 9 ) + + UPDATE users + SET field1='value1', + field2='value2', + field3='value3' + WHERE field1=1 + + -- UPSERT + CREATE TABLE phonebook2( + name TEXT PRIMARY KEY, + phonenumber TEXT, + validDate DATE + ); + INSERT INTO phonebook2(name,phonenumber,validDate) + VALUES('Alice','704-555-1212','2018-05-08') + ON CONFLICT(name) DO UPDATE SET + phonenumber=excluded.phonenumber, + validDate=excluded.validDate + WHERE excluded.validDate>phonebook2.validDate; + */ + + /** + CREATE TABLE phonebook2( + name TEXT PRIMARY KEY, + phonenumber TEXT, + validDate DATE + ); + */ + func sql(create table: String, pkey: String = "id") -> String { + [String]() { + "CREATE TABLE \(table) (" + for p in md.properties { + Joined(with: "") { + "\t" + sql_decl(for: p) + if p.name == pkey { " PRIMARY KEY"} + if p.name != md.properties.last?.name { "," } +// .append(",", if: { p.name != md.properties.last?.name }) + + } + } + ")" + } + .joined(separator: "\n") + } + + func sql(select table: String, where cond: String? = nil, limit: Int = 0) -> String { + [String]() { + "SELECT" + md.properties.map(\.name).joined(separator: ", ") + "FROM \(table)" + } + .joined(separator: " ") + } + + func sql(insert table: String) -> String { + [String]() { + "INSERT INTO \(table) (" + md.properties.map(\.name).joined(separator: ", ") + ") VALUES (" + String(repeating: "?, ", count: md.properties.count - 1) + "?)" + } + .joined(separator: " ") + } +} + +extension Schema { + + func type_decl(for md: Metadata) -> String { + switch md.type { + case is Bool.Type: + return "BOOL" + case is Date.Type: + return "DATE" + case is Data.Type: + return "BLOB" + case is String.Type: + return "TEXT" + case is ArrayProtocol.Type: + return "LIST" + case is any BinaryFloatingPoint.Type: + return "REAL" + case is any FixedWidthInteger.Type: + return "INT" + + case let it as any OptionalProtocol.Type: + return type_decl(for: swift_metadata(of: it.honestType)) + + default: + return "JSON" + } + } + + func type_decl(for p: Metadata.Property) -> String { + type_decl(for: p.metadata) + } + + func sql_decl(for p: Metadata.Property, strict: Bool = false) -> String { + if strict { + return "\(p.name) \(type_decl(for: p))\(p.isOptional ? "" : " NOT NULL")" + } else { + return "\(p.name) \(type_decl(for: p))" + } + } +} + +// MARK: - SQLConnection Extensions +public struct SQLCursor { + var stmt: SQLStatement + public func next() throws -> T? { + guard try stmt.step() else { return nil } + return try stmt.instantiate(T.self) + } +} + +public extension SQLConnection { + func select( + _ type: T.Type = T.self, + from table: String + ) throws -> SQLCursor { + let sc = Schema(for: T.self) + let sql = sc.sql(select: table) + print(#function, sql) + let select = try prepare(sql) + return SQLCursor(stmt: select) + } +} + + +// MARK: - SQLStatement Extensions +public extension SQLStatement { + + func rebind(_ nob: T) throws -> SQLStatement { + var copy = nob + try reset() + return try bind(©) + } + + + func bind(_ nob: inout T) throws -> SQLStatement { + var params = [Storable?]() + let md = swift_metadata(of: nob) + for p in md.properties { + let v = swift_value(of: &nob, key: p.name) + if let v = v as? BuiltinStorable { + params.append((v.builtinRepresentation as! Storable)) + } else if let v = v as? Storable { + params.append(v.storableRepresentation) + } else { + params.append(nil) + } + } + try self.bind(params) + return self + } + + func instantiate( + _ type: T.Type = T.self, + strict: Bool = false + ) throws -> T? { + + var it: T = .defaultValue() + let md = swift_metadata(of: T.self) + + for p in md.properties { + var v: Any? + if strict { + v = try self.value(named: p.name, as: p.metadata.type) + } else { + v = try? self.value(named: p.name, as: p.metadata.type) + } + swift_setValue(v, to: &it, key: p.name) + } + return it + } + + func instantiate( + _ type: T.Type = T.self, + strict: Bool = false + ) throws -> T { + + var it: T = .defaultValue() + let md = swift_metadata(of: T.self) + + for p in md.properties { + var v: Any? + if strict { + v = try self.value(named: p.name, as: p.metadata.type) + } else { + v = try? self.value(named: p.name, as: p.metadata.type) + } + swift_setValue(v, to: &it, key: p.name) + } + return it + } +} + +// MARK: - Metadata Extensions +extension Metadata.Property: Hashable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name + } + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } +} + +public func Joined(with sep: String = "", @ArrayBuilder f: () -> [String]) -> String { + f().joined(separator: sep) +} + +// MARK: - Helper and Extensions +public extension Metadata { + var isOptional: Bool { kind == .optional } +} + +extension Metadata.Property { + var isOptional: Bool { metadata.isOptional } +} + +public func tab(_ cnt: Int = 1) -> String { + cnt == 1 ? "\t" : String(repeating: "\t", count: cnt) +} + +public func newline(_ cnt: Int = 1) -> String { + cnt == 1 ? "\t" : String(repeating: "\n", count: cnt) +} + + +public extension String { + func append(_ s: String, if cond: () -> Bool) -> String { + cond() ? self + s : self + } + + func transform(if cond: () -> Bool, fn: (String) -> String) -> String { + cond() ? fn(self) : self + } + + func `if`(_ cond: () -> Bool, fn: (String) -> String) -> String { + cond() ? fn(self) : self + } + +} + +public extension Array { + init(@ArrayBuilder makeItems: () -> [Element]) { + self.init(makeItems()) + } +} diff --git a/Sources/SwiftSQLExt/SwiftSQLExt.swift b/Sources/SwiftSQLExt/SwiftSQLExt.swift deleted file mode 100644 index 04f86ff..0000000 --- a/Sources/SwiftSQLExt/SwiftSQLExt.swift +++ /dev/null @@ -1,67 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2020 Alexander Grebenyuk (github.com/kean). - -import Foundation -import SQLite3 -import SwiftSQL - -public extension SQLStatement { - /// Fetches the next row. - func row(_ type: T.Type) throws -> T? { - guard try step() else { - return nil - } - return try T(row: SQLRow(statement: self)) - } - - /// Fetches the first `count` rows returned by the statement. By default, - /// fetches all rows. - func rows(_ type: T.Type, count: Int? = nil) throws -> [T] { - var objects = [T]() - let limit = count ?? Int.max - while let object = try row(T.self), 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 - } - - /// Returns a single column of the current result row of a query. - /// - /// If the SQL statement does not currently point to a valid row, or if the - /// 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 { - statement.column(at: index) - } - - /// Returns a single column of the current result row of a query. If the - /// value is `Null`, returns `nil.` - /// - /// If the SQL statement does not currently point to a valid row, or if the - /// 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? { - statement.column(at: index) - } -} - -public protocol SQLRowDecodable { - init(row: SQLRow) throws -} diff --git a/Sources/SwiftSQLTesting/SqliteSnapshotTesting.swift b/Sources/SwiftSQLTesting/SqliteSnapshotTesting.swift new file mode 100644 index 0000000..20486dd --- /dev/null +++ b/Sources/SwiftSQLTesting/SqliteSnapshotTesting.swift @@ -0,0 +1,90 @@ +// +// SqliteSnapshotTesting.swift +// +// +// Created by Jason Jobe on 2/20/23. +// + +#if canImport(SnapshotTesting) +import Foundation +import SnapshotTesting +import SwiftSQL + +extension Snapshotting where Value: SQLConnection, Format == String { + static func _dump(_ table: String? = nil) -> Snapshotting { + return SimplySnapshotting.lines.pullback { (db: SQLConnection) in + do { + return try DatabaseDumper(db, table: table).dump() + } catch { + return "Error: " + error.localizedDescription + } + } + } +} + +/* +public extension Snapshotting where Value == DatabaseDumper, Format == String { + static func _dump() -> Snapshotting { + return SimplySnapshotting.lines.pullback { (dumper) in + do { + return try dumper.dump() + } catch { + return "Error: " + error.localizedDescription + } + } + } +} +*/ + +struct DatabaseDumper { + var db: SQLConnection + var table: String? + + init(_ db: SQLConnection, table: String? = nil) { + self.db = db + self.table = table + } + + func dump() throws -> String { + guard let table else { + throw NSError(domain: "sql.test", code: 0) + } +// let encoder = JSONEncoder() +// encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let select = try db.prepare("SELECT * FROM \(table)") + + var result = "# \(table)\n" +// let cols = select.columnNames + + while try select.step() { + let row = select.keyValuePairs + print("(", terminator: "", to: &result) + let line = row.map { "\($0): \($1)" }.joined(separator: ", ") + print(line, terminator: ")\n", to: &result) + } + print("\n# EOF", to: &result) + return result + } +} + +public extension Snapshotting where Value == SQLConnection, Format == String { + /// Snapshot strategy for comparing databases based on dump representation. +// static let dbDump = _dump() + static func dbDumpTable(_ table: String) -> Self { + _dump(table) + } +} + +/* +public extension Snapshotting where Value == DatabaseQueue, Format == String { + /// Snapshot strategy for comparing databases based on dump representation. + static let dbDump = _dump() +} + +public extension Snapshotting where Value == DatabasePool, Format == String { + /// Snapshot strategy for comparing databases based on dump representation. + static let dbDump = _dump() +} +*/ +#endif + diff --git a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift b/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift deleted file mode 100644 index 08df71d..0000000 --- a/Tests/SwiftSQLExtTests/SwiftSQLExtTests.swift +++ /dev/null @@ -1,116 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2020 Alexander Grebenyuk (github.com/kean). - -import XCTest -import Foundation -import SwiftSQL -import SwiftSQLExt - -final class SwiftSQLExtTests: XCTestCase { - var tempDir: TempDirectory! - var db: SQLConnection! - - override func setUp() { - super.setUp() - - tempDir = try! TempDirectory() - db = try! SQLConnection(location: .disk(url: tempDir.file(named: "test-statements"))) - } - - override func tearDown() { - super.tearDown() - - try! tempDir.destroy() - } - - // MARK: Rows - - func testRow() throws { - // GIVEN - try db.populateStore() - - // WHEN - let user = try db - .prepare("SELECT Name, Level FROM Users ORDER BY Level ASC") - .row(User.self) - - // THEN - XCTAssertEqual(user, User(name: "Alice", level: 80)) - } - - func testRows() throws { - // GIVEN - try db.populateStore() - - // WHEN - let users = try db - .prepare("SELECT Name, Level FROM Users ORDER BY Level ASC") - .rows(User.self) - - // THEN - XCTAssertEqual(users, [ - User(name: "Alice", level: 80), - User(name: "Bob", level: 90) - ]) - } - - func testFirstNRows() throws { - // GIVEN - try db.populateStore() - - // WHEN - let users = try db - .prepare("SELECT Name, Level FROM Users ORDER BY Level ASC") - .rows(User.self, count: 1) - - // THEN - XCTAssertEqual(users, [ - User(name: "Alice", level: 80) - ]) - } -} - -private extension SQLConnection { - func populateStore() throws { - try execute(""" - CREATE TABLE Users - ( - Id INTEGER PRIMARY KEY NOT NULL, - Name VARCHAR, - Level INTEGER - ) - """) - - let statement = try self.prepare(""" - INSERT INTO Users (Name, Level) - VALUES (?, ?) - """) - - try statement - .bind("Alice", Int64(80)) - .execute() - - try statement.reset() - - try statement - .bind("Bob", Int64(90)) - .execute() - } -} - - -private struct User: 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[0] - self.level = row[1] - } -} diff --git a/Tests/SwiftSQLTests/PerformanceTests.swift b/Tests/SwiftSQLTests/PerformanceTests.swift index d01613a..db0c7f6 100644 --- a/Tests/SwiftSQLTests/PerformanceTests.swift +++ b/Tests/SwiftSQLTests/PerformanceTests.swift @@ -37,7 +37,6 @@ final class PerformanceTests: XCTestCase { INSERT INTO Users (Name, Surname, Level) VALUES (?, ?, ?) """) - measure { for _ in 0..<500 { try! statement diff --git a/Tests/SwiftSQLTests/QueryTests.swift b/Tests/SwiftSQLTests/QueryTests.swift new file mode 100644 index 0000000..e4cd721 --- /dev/null +++ b/Tests/SwiftSQLTests/QueryTests.swift @@ -0,0 +1,179 @@ +// +// QueryTests.swift +// +// +// Created by Jason Jobe on 2/5/23. +// + +import XCTest +import SwiftSQL +import SwiftSQLExt +//import KeyValueCoding +#if canImport(SnapshotTesting) +import SnapshotTesting +#endif + +final class QueryTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testQuery() throws { + let db = try SQLConnection(location: .memory()) + @Query(db: db) var o_topic: Topic? + @Query(db: db) var topic: Topic + @Query(db: db) var topics: [Topic] = [] + + $topic.search = "foo" +// $topic.wrappedValue.set("foo", to: "") + + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } +} + +import SwiftUI +import Combine + +extension SQLConnection: EnvironmentKey { + public static let defaultValue: SQLConnection + = try! SQLConnection(location: .memory()) +} + +extension EnvironmentValues { + public var dataStore: SQLConnection { + get { + self[SQLConnection.self] + } set { + self[SQLConnection.self] = newValue + } + } +} + +@propertyWrapper +@dynamicMemberLookup +class Box: ObservableObject { + var wrappedValue: A + + init(_ wrappedValue: A) { + self.wrappedValue = wrappedValue + } + + subscript(dynamicMember keyp: WritableKeyPath) -> V { + get { wrappedValue[keyPath: keyp] } + set { wrappedValue[keyPath: keyp] = newValue } + } +} + +@propertyWrapper +struct Query: DynamicProperty { + typealias Value = Value + @Environment(\.dataStore) var dataStore: SQLConnection + @State var db: SQLConnection? + // A state object that we notify of updates + @StateObject private var watcher: Watcher + + init(wrappedValue: Value, db: SQLConnection) { + self.db = db + self._watcher = .init(wrappedValue: Watcher(db: db)) + } + + init(db: SQLConnection) { + self.db = db + self._watcher = .init(wrappedValue: Watcher(db: db)) + } + + func update() { +// guard db != dataStore else { return } +// self.watcher = Watcher(db: db) + } + + var wrappedValue: Value { + get { + watcher.value! + } + nonmutating set { + // Tell SwiftUI we're going to change something + watcher.notifyUpdate() + // Your setter code here + } + } + + public var projectedValue: Box { + get { Box(watcher.cond) } + set { print(newValue) } + } + +// public var projectedValue: Binding { +// return Binding(get: { watcher.cond }, +// set: { _ in } +// ) +// } +// public var projectedValue: Binding { +// return Binding(get: { core.filter ?? baseFilter }, +// set: { +// if core.filter != $0 { +// core.objectWillChange.send() +// core.filter = $0 +// } +// }) +// } + + class Watcher: ObservableObject { + var db: SQLConnection + var value: Value? + var task: Task? + var cond: SQLPredicate = .init(search: "") + + init(db: SQLConnection) { + self.db = db + } + + deinit { + task?.cancel() + } + + func notifyUpdate() { + objectWillChange.send() + } + } +} + +public struct SQLPredicate: Equatable { + public static func == (lhs: SQLPredicate, rhs: SQLPredicate) -> Bool { + lhs.search == rhs.search + } + + var _search: String + var search: String { + get { _search } + set { _search = newValue } + } + + init(search: String) { + self._search = search + } + + func callAsFunction(_ key: String) { + + } + + var fn: (_ key: String, _ value: String) -> Void = { (k, v) in } + + func set(_ key: String, to value: String) { + + } + + subscript(_ key: String) -> String { + get { search } + set { search = newValue } + } +} diff --git a/Tests/SwiftSQLTests/SQLSchemaTests.swift b/Tests/SwiftSQLTests/SQLSchemaTests.swift new file mode 100644 index 0000000..9c7aaa4 --- /dev/null +++ b/Tests/SwiftSQLTests/SQLSchemaTests.swift @@ -0,0 +1,312 @@ +// +// SQLSchemaTests.swift +// +// +// Created by Jason Jobe on 1/30/23. +// + +import XCTest +@testable import SwiftSQL +@testable import SwiftSQLExt +import KeyValueCoding +import SnapshotTesting + +protocol Entity {} + +final class SQLSchemaTests: XCTestCase { + + override func setUpWithError() throws { + // Set `isRecording` to reset Snapshots +// isRecording = true + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func sampleDatabase() throws -> SQLConnection { + let db = try! SQLConnection(location: .memory()) + try db.execute("CREATE TABLE Test (name TEXT, ndx INT)") + + // WHEN/THEN binds the value + try db.prepare("INSERT INTO Test (name, ndx) VALUES (?, ?)") + .bind("alpha", 1) + .execute() + return db + } + + func testInstantiateStrict() throws { + // We delibertly do NOT select all columns + // So it should raise an exception + let db = try sampleDatabase() + let statement = try db.prepare("SELECT name FROM Test") + XCTAssertTrue(try statement.step()) + struct S: ExpressibleByDefault { + var name: String + var ndx: Int + + init(defaultContext: ()) { + name = "" + ndx = 0 + } + } + let s = Schema(for: S.self) + do { + let v: S = try s.instantiate(from: statement, strict: true) + assertSnapshot(matching: v, as: .dump) + } catch { + print(error.localizedDescription) + } + } + + func testInstantiateLoose() throws { + // We delibertly do NOT select all columns + // So some default values should remain + let db = try sampleDatabase() + let statement = try db.prepare("SELECT name FROM Test") + XCTAssertTrue(try statement.step()) + struct S: ExpressibleByDefault { + var name: String + var ndx: Int + + init(defaultContext: ()) { + name = "" + ndx = 0 + } + } + let s = Schema(for: S.self) + let v: S = try s.instantiate(from: statement, strict: false) + assertSnapshot(matching: v, as: .dump) + } + + func testInstantiate() throws { + let db = try sampleDatabase() + let statement = try db.prepare("SELECT name, ndx FROM Test") + XCTAssertTrue(try statement.step()) + struct S: ExpressibleByDefault { + var name: String + var ndx: Int + + init(defaultContext: ()) { + name = "" + ndx = 0 + } + } + let s = Schema(for: S.self) + let v: S = try s.instantiate(from: statement) + assertSnapshot(matching: v, as: .dump) + } + + func testAPI() throws { + let a = [23] + let t = type(of: a as ArrayProtocol) + var s: Int? = 23 + let sr = s.storableRepresentation + s = nil + let sn = s.storableRepresentation + assertSnapshot(matching: (s, sr, sn), as: .dump) + assertSnapshot(matching: (t.elementType, t.empty()), as: .dump) + } + + // FIXME: Add JSON Column support + func testTopicII() throws { + let db = try! SQLConnection(location: .memory()) + let sc = Schema(table: "people", for: Person.self) + try sc.create(in: db, table: "people") + + let date = Date(timeIntervalSince1970: 0) + let p1 = Person(id: 10, name: "George", dob: date, tags: ["one"], friends: []) + let p2 = Person(id: 20, name: "Jane", dob: date, tags: ["two"], friends: []) + try sc.insert(in: db, [p1, p2]) + + try sc.select(in: db, where: "", limit: 1) { + print($0) + } + + assertSnapshot(matching: db, as: .dbDumpTable("people")) + } + + func testTopic() throws { + let db = try! SQLConnection(location: .memory()) + let s = Schema(for: Topic.self) + + // CREATE + let sql = s.sql(create: "topic") + print(sql) + try db.execute(sql) + + // INSERT + let insert = try db.prepare(s.sql(insert: "topic")) + + try insert + .bind(1, "alpha", 23) + .execute() + + // SELECT + let select = try db.prepare(s.sql(select: "topic")) + + while try select.step() { + let t: Topic = try s.instantiate(from: select, strict: false) + print (t) + } + + let t1 = Topic(id: "10", name: "beta") + try insert.rebind(t1).execute() + + let t2 = Topic(id: "20", name: "charlie") + try insert.rebind(t2).execute() + + try select.reset() + var results = [Any]() + + while try select.step() { + let t: Topic = try s.instantiate(from: select, strict: false) + results.append(t) + print (t) + } + + assertSnapshot(matching: results, as: .dump) + + let curs = try db.select(Topic.self, from: "topic") + while let row = try curs.next() { + print(row) + } + } + + func testPragma() throws { + let db = try! SQLConnection(location: .memory()) + let ts = Schema(for: Topic.self) + + // CREATE + let sql = ts.sql(create: "topic") + try db.execute(sql) + + let info = try db.prepare("PRAGMA table_info(topic)") + + while try info.step() { + do { + let t: Table = try info.instantiate(strict: false) + print (t) + } catch { + let p = info.dictionaryValue + print (error, p) + } + } + } + + func _testTableInfo() throws { + let db = try! SQLConnection(location: .memory()) + let ts = Schema(for: Topic.self) + + // CREATE + let sql = ts.sql(create: "topic") + try db.execute(sql) + + // ERROR notnull is keyword + let curs = try db.select(Table.self, from: "pragma_table_info('topic')") + while let r = try curs.next() { + print(r) + } + } + + func testSchemaSQLCreate() throws { + let s = Schema(for: Person.self) + assertSnapshot(matching: s.sql(create: "person"), as: .lines) + } + + func testSchemaSQLSelect() throws { + let s = Schema(for: Person.self) + assertSnapshot(matching: s.sql(select: "person"), as: .lines) + } + + func testSchemaSQLInsert() throws { + let s = Schema(for: Person.self) + assertSnapshot(matching: s.sql(insert: "person"), as: .lines) + } +} + +struct Table: ExpressibleByDefault { + init(defaultContext: ()) { + cid = 0 + name = "" + type = "" + notnull = false + dflt_value = nil + pk = false + } + + var cid: Int64 + var name: String + var type: String + var notnull: Bool + var dflt_value: Any? + var pk: Bool +} + +//extension UUID { +// static func preview(_ ndx: Int) -> UUID { +// return .init(uuidString: "\ndx")! +// } +//} +// let v = _swift_getKeyPath(pattern: , arguments: ) + +struct TopicQuery: EntityQuery { + func entities(for identifiers: [Topic.ID]) async throws -> [Topic] { + .init() + } +} + +/* + func suggestedEntities() async throws -> [AlbumEntity] { + try await MusicCatalog.shared.favoriteAlbums() + .map { AlbumEntity(id: $0.id, albumName: $0.name) } + } + + */ +import AppIntents + +@available(macOS 13.0, *) +struct Topic: AppEntity { + typealias ID = String + static var defaultQuery: TopicQuery = .init() + + var id: ID + var name: String + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + "Topic" + } + + var displayRepresentation: DisplayRepresentation { + .init(title: LocalizedStringResource(stringLiteral: name)) + } + +} + +//struct Topic { +// var id: Int64 +// var name: String +// var value: Int? +//} + +extension Topic: ExpressibleByDefault { + init(defaultContext: ()) { + id = .init() + name = "" + // value = nil + } +} + +struct Person { + var id: Int64 + var name: String + var dob: Date? + var tags: [String] + var friends: [Person] +} + +extension Person: ExpressibleByDefault { + init(defaultContext: ()) { + self = .init(id: 0, name: "", tags: [], friends: []) + } +} diff --git a/Tests/SwiftSQLTests/SQLStatementTests.swift b/Tests/SwiftSQLTests/SQLStatementTests.swift index e333f5d..5cff12d 100644 --- a/Tests/SwiftSQLTests/SQLStatementTests.swift +++ b/Tests/SwiftSQLTests/SQLStatementTests.swift @@ -44,21 +44,27 @@ final class SQLStatementTests: XCTestCase { try db.createTables() let insert = try db.prepare(""" - INSERT INTO Users (Level, Name) - VALUES (?, ?) + INSERT INTO Users (Level, Name, blob) + VALUES (?, ?, ?) """) // WHEN + let v: Any? = 80 try insert - .bind(80, at: 0) + .bind(v, at: 0) .bind("Alex", at: 1) + .bind(Data(), at: 2) .execute() + let cnt = insert.bindParameterCount + XCTAssertEqual(cnt, 3) + // THEN - let select = try db.prepare("SELECT Level, Name FROM Users") + let select = try db.prepare("SELECT Level, Name, blob FROM Users") XCTAssertTrue(try select.step()) XCTAssertEqual(select.column(at: 0), 80) XCTAssertEqual(select.column(at: 1), "Alex") + XCTAssertEqual(select.column(at: 2), Data()) } func testBindNilUsingIndexes() throws { @@ -73,7 +79,7 @@ final class SQLStatementTests: XCTestCase { // WHEN try statement .bind(80, at: 0) - .bind(nil as String?, at: 1) + .bind(nil, at: 1) .execute() // THEN @@ -264,7 +270,7 @@ final class SQLStatementTests: XCTestCase { let statement = try db.prepare("SELECT * FROM Users") // THEN - XCTAssertEqual(statement.columnCount, 3) + XCTAssertEqual(statement.columnCount, 4) } func testColumnNameAtIndex() throws { @@ -277,22 +283,23 @@ final class SQLStatementTests: XCTestCase { } } -private extension SQLConnection { +extension SQLConnection { func createTables() throws { try execute(""" CREATE TABLE Users ( Id INTEGER PRIMARY KEY NOT NULL, Name VARCHAR, - Level INTEGER + Level INTEGER, + blob BLOB ) """) } func populateStore() throws { let statement = try self.prepare(""" - INSERT INTO Users (Name, Level) - VALUES (?, ?) + INSERT INTO Users (Name, Level, blob) + VALUES (?, ?, ?) """) try statement diff --git a/Tests/SwiftSQLTests/SQLUnitTests.swift b/Tests/SwiftSQLTests/SQLUnitTests.swift new file mode 100644 index 0000000..816a18d --- /dev/null +++ b/Tests/SwiftSQLTests/SQLUnitTests.swift @@ -0,0 +1,165 @@ +// +// SQLUnitTests.swift +// +// +// Created by Jason Jobe on 1/29/23. +// + +import SnapshotTesting +import XCTest +@testable import SwiftSQL +@testable import SwiftSQLTesting + +import SQLite3 + +final class SQLUnitTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + // Snapshot Testing reset +// isRecording = true + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testDBSnapshot() throws { + let db = try SQLConnection(location: .memory()) + try populate(db: db) + assertSnapshot(matching: db, as: .dbDumpTable("test")) + } + + func testSelect() throws { + + let db = try SQLConnection(location: .memory()) + try populate(db: db) + + // THEN + let last = db.lastInsertRowID + XCTAssert(last == 80) + + let count = db.lastChangeCount +// let count = insert.bindParameterCount + XCTAssert(count == 1) + + let select = try db.prepare("SELECT * FROM test") + XCTAssertTrue(try select.step()) + let row = select.dictionaryValue + assertSnapshot(matching: row, as: .dump) + + let str: String = try select.value(at: 1) + let int: Int = try select.value(at: 0) + let real: Double = try select.value(at: 3) + let data: Data = try select.value(at: 4) + assertSnapshot(matching: (data, str, real, int), as: .dump) + } + + func testDatabasePublisher() throws { + let db = try SQLConnection(location: .memory()) + var log: [String] = [] + let can = db.publisher().sink { + print($0) + log.append("\($0)") + } + + try db.execute("CREATE TABLE Test (Field VARCHAR)") + try db.execute("INSERT INTO Test VALUES ('Howdy')") + // try db.execute("COMMIT") + try db.execute(""" + BEGIN; + INSERT INTO Test VALUES ('Howdy'); + ROLLBACK; + """) + XCTAssertNotNil(can) + assertSnapshot(matching: log, as: .dump) + print ("Pass", #function) + } + + func testDatabaseHooks() throws { + let db = try SQLConnection(location: .memory()) + var didCommit = false + var didRollback = false + + db.createCommitHandler { + print("commit") + didCommit = true + } + db.createRollbackHandler { + print("rollback") + didRollback = true + } + db.createUpdateHandler { info in + print(info, + info.isDelete, + info.isInsert, + info.isUpdate) + assertSnapshot(matching: info, as: .dump) + } + + try db.execute("CREATE TABLE Test (Field VARCHAR)") + try db.execute("INSERT INTO Test VALUES ('Howdy')") +// try db.execute("COMMIT") + try db.execute(""" + BEGIN; + INSERT INTO Test VALUES ('Howdy'); + ROLLBACK; + """) + + db.removeCommitHandler() + db.removeUpdateHandler() + db.removeRollbackHandler() + + XCTAssertTrue(didCommit) + XCTAssertTrue(didRollback) + + db.interrupt() + print ("Pass", #function) + } + + func testSQLErrors() throws { + let db = try SQLConnection(location: .memory()) + try db.execute("CREATE TABLE Test (Field VARCHAR)") + + let e1 = SQLError(code: 0, message: "E1 Error") + assertSnapshot(matching: e1, as: .dump) + + let e2 = SQLError(code: SQLITE_ROW, db: db.ref) + assertSnapshot(matching: e2, as: .dump) + + let e3 = SQLError(code: 666, db: db.ref) + assertSnapshot(matching: e3, as: .dump) + + } + + func populate(db: SQLConnection) throws { + /// GIVEN + try db.execute(""" + CREATE TABLE test + ( + Id INTEGER PRIMARY KEY NOT NULL, + Name VARCHAR, + Level INTEGER, + number REAL, + thunk BLOB + ) + + """) + + let insert = try db.prepare(""" + INSERT INTO test (id, level, name, number, thunk) + VALUES (?, ?, ?, ?, ?) + """) + + // WHEN + let d = "foo".data(using: .ascii)! + + try insert + .bind(80, at: 0) + .bind("Alex", at: 1) + .bind(66, at: 2) + .bind(43.5, at: 3) + .bind(d, at: 4) + .execute() + } +} diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testAPI.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testAPI.1.txt new file mode 100644 index 0000000..84d85e7 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testAPI.1.txt @@ -0,0 +1,4 @@ +▿ (3 elements) + - .0: Optional.none + - .1: 23 + - .2: Optional.none diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testAPI.2.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testAPI.2.txt new file mode 100644 index 0000000..245e1e0 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testAPI.2.txt @@ -0,0 +1,3 @@ +▿ (2 elements) + - .0: Int + - .1: 0 elements diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testInstantiate.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testInstantiate.1.txt new file mode 100644 index 0000000..1f0a034 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testInstantiate.1.txt @@ -0,0 +1,3 @@ +▿ S + - name: "alpha" + - ndx: 1 diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testInstantiateLoose.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testInstantiateLoose.1.txt new file mode 100644 index 0000000..010d4df --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testInstantiateLoose.1.txt @@ -0,0 +1,3 @@ +▿ S + - name: "alpha" + - ndx: 0 diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLCreate.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLCreate.1.txt new file mode 100644 index 0000000..2172c6a --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLCreate.1.txt @@ -0,0 +1,7 @@ +CREATE TABLE person ( + id INT PRIMARY KEY, + name TEXT, + dob DATE, + tags LIST, + friends LIST +) \ No newline at end of file diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLInsert.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLInsert.1.txt new file mode 100644 index 0000000..9e79711 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLInsert.1.txt @@ -0,0 +1 @@ +INSERT INTO person ( id, name, dob, tags, friends ) VALUES ( ?, ?, ?, ?, ?) \ No newline at end of file diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLSelect.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLSelect.1.txt new file mode 100644 index 0000000..9d8af73 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testSchemaSQLSelect.1.txt @@ -0,0 +1 @@ +SELECT id, name, dob, tags, friends FROM person \ No newline at end of file diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testTopic.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testTopic.1.txt new file mode 100644 index 0000000..a3e0732 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testTopic.1.txt @@ -0,0 +1,10 @@ +▿ 3 elements + ▿ Topic + - id: "1" + - name: "alpha" + ▿ Topic + - id: "10" + - name: "beta" + ▿ Topic + - id: "20" + - name: "charlie" diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testTopicII.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testTopicII.1.txt new file mode 100644 index 0000000..72e60b2 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLSchemaTests/testTopicII.1.txt @@ -0,0 +1,5 @@ +# people +(id: 10, name: George, dob: 1.3964861880825545e+19, tags: nil, friends: nil) +(id: 20, name: Jane, dob: 1.3964861880825545e+19, tags: nil, friends: nil) + +# EOF diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testBinders.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testBinders.1.txt new file mode 100644 index 0000000..8e4429e --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testBinders.1.txt @@ -0,0 +1,6 @@ +▿ SQLBinder + - getf: (Function) + ▿ name: Optional + - some: "id" + - setf: (Function) + - valueType: Int64 diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testBinders.2.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testBinders.2.txt new file mode 100644 index 0000000..e0f0c32 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testBinders.2.txt @@ -0,0 +1,16 @@ +▿ 3 elements + ▿ SQLBinder + - getf: (Function) + - name: Optional.none + - setf: (Function) + - valueType: Int + ▿ SQLBinder + - getf: (Function) + - name: Optional.none + - setf: (Function) + - valueType: Int32 + ▿ SQLBinder + - getf: (Function) + - name: Optional.none + - setf: (Function) + - valueType: Float diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDBSnapshot.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDBSnapshot.1.txt new file mode 100644 index 0000000..5644af4 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDBSnapshot.1.txt @@ -0,0 +1,4 @@ +# test +(id: 80, name: 66, level: Alex, number: 43.5, thunk: 3 bytes) + +# EOF diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabaseHooks.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabaseHooks.1.txt new file mode 100644 index 0000000..374636c --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabaseHooks.1.txt @@ -0,0 +1,5 @@ +▿ main:Test insert(1) + - database: "main" + - tableName: "Test" + - op_code: 18 + - rowid: 1 diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabaseHooks.2.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabaseHooks.2.txt new file mode 100644 index 0000000..97344f9 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabaseHooks.2.txt @@ -0,0 +1,5 @@ +▿ main:Test insert(2) + - database: "main" + - tableName: "Test" + - op_code: 18 + - rowid: 2 diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabasePublisher.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabasePublisher.1.txt new file mode 100644 index 0000000..6071df6 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testDatabasePublisher.1.txt @@ -0,0 +1,6 @@ +▿ 5 elements + - "didCommit" + - "didUpdate(main:Test insert(1))" + - "didCommit" + - "didUpdate(main:Test insert(2))" + - "didRollback" diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.1.txt new file mode 100644 index 0000000..c337538 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.1.txt @@ -0,0 +1,3 @@ +▿ SQLError + - code: 0 + - message: "E1 Error" diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.2.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.2.txt new file mode 100644 index 0000000..ebb1e8c --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.2.txt @@ -0,0 +1 @@ +- Optional.none diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.3.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.3.txt new file mode 100644 index 0000000..659a285 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSQLErrors.3.txt @@ -0,0 +1,4 @@ +▿ Optional + ▿ some: SQLError + - code: 666 + - message: "not an error" diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSelect.1.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSelect.1.txt new file mode 100644 index 0000000..1305c08 --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSelect.1.txt @@ -0,0 +1,16 @@ +▿ 5 key/value pairs + ▿ (2 elements) + - key: "id" + - value: 80 + ▿ (2 elements) + - key: "level" + - value: "Alex" + ▿ (2 elements) + - key: "name" + - value: "66" + ▿ (2 elements) + - key: "number" + - value: 43.5 + ▿ (2 elements) + - key: "thunk" + - value: 3 bytes diff --git a/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSelect.2.txt b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSelect.2.txt new file mode 100644 index 0000000..c804cfc --- /dev/null +++ b/Tests/SwiftSQLTests/__Snapshots__/SQLUnitTests/testSelect.2.txt @@ -0,0 +1,5 @@ +▿ (4 elements) + - .0: 3 bytes + - .1: "66" + - .2: 43.5 + - .3: 80