diff --git a/.swiftlint.yml b/.swiftlint.yml index b5e53d56..34bb253b 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -38,5 +38,6 @@ line_length: ignores_comments: true file_length: - warning: 900 - error: 900 + warning: 500 + error: 500 + ignore_comment_only_lines: true diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index b30c18d2..5bb52f4e 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; }; 19A17073552293CA063BEA66 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17E723300E5ED3771DCB5 /* Result.swift */; }; 19A1709C3E7A406E62293B2A /* Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17B93B48B5560E6E51791 /* Fixtures.swift */; }; + 19A170ACC97B19730FB7BA4D /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA55DABB480F9020C8A /* DateAndTimeFunctions.swift */; }; 19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; 19A171967CC511C4F6F773C9 /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; @@ -67,6 +68,7 @@ 19A174D78559CD30679BCCCB /* FTS5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1721B8984686B9963B45D /* FTS5Tests.swift */; }; 19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1730E4390C775C25677D1 /* FTS5.swift */; }; 19A175DFF47B84757E547C62 /* fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 19A17E2695737FAB5D6086E3 /* fixtures */; }; + 19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; 19A176406BDE9D9C80CC9FA3 /* QueryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17BA6B4E282C1315A115C /* QueryIntegrationTests.swift */; }; 19A1769C1F3A7542BECF50FF /* DateAndTimeFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */; }; 19A177CC33F2E6A24AF90B02 /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; @@ -75,6 +77,7 @@ 19A1785195182AF8731A8BDA /* RowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */; }; 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A1710E73A46D5AC721CDA9 /* Errors.swift */; }; 19A179A0C45377CB09BB358C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; + 19A179B59450FE7C4811AB8A /* Connection+Aggregation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */; }; 19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; 19A179E76EA6207669B60C1B /* Cipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A178A39ACA9667A62663CC /* Cipher.swift */; }; 19A17C4B951CB054EE48AB1C /* CipherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19A17399EA9E61235D5D77BF /* CipherTests.swift */; }; @@ -237,6 +240,7 @@ 19A1729B75C33F9A0B9A89C1 /* DateAndTimeFunctionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateAndTimeFunctionTests.swift; sourceTree = ""; }; 19A1730E4390C775C25677D1 /* FTS5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS5.swift; sourceTree = ""; }; 19A17399EA9E61235D5D77BF /* CipherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CipherTests.swift; sourceTree = ""; }; + 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Connection+Aggregation.swift"; sourceTree = ""; }; 19A175C1F9CB3BBAB8FCEC7B /* RowTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RowTests.swift; sourceTree = ""; }; 19A178A39ACA9667A62663CC /* Cipher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cipher.swift; sourceTree = ""; }; 19A1794B7972D14330A65BBD /* Linux.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Linux.md; sourceTree = ""; }; @@ -462,6 +466,7 @@ 19A1710E73A46D5AC721CDA9 /* Errors.swift */, 02A43A9722738CF100FEC494 /* Backup.swift */, 19A17E723300E5ED3771DCB5 /* Result.swift */, + 19A175A9CB446640AE6F2200 /* Connection+Aggregation.swift */, ); path = Core; sourceTree = ""; @@ -850,6 +855,7 @@ 19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */, 19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */, 19A17073552293CA063BEA66 /* Result.swift in Sources */, + 19A179B59450FE7C4811AB8A /* Connection+Aggregation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -945,6 +951,7 @@ 19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */, 19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */, 19A173EFEF0B3BD0B3ED406C /* Result.swift in Sources */, + 19A176376CB6A94759F7980A /* Connection+Aggregation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1008,6 +1015,7 @@ 19A17490543609FCED53CACC /* Errors.swift in Sources */, 19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */, 19A17F1B3F0A3C96B5ED6D64 /* Result.swift in Sources */, + 19A170ACC97B19730FB7BA4D /* Connection+Aggregation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/SQLite/Core/Connection+Aggregation.swift b/Sources/SQLite/Core/Connection+Aggregation.swift new file mode 100644 index 00000000..106f292c --- /dev/null +++ b/Sources/SQLite/Core/Connection+Aggregation.swift @@ -0,0 +1,155 @@ +import Foundation +#if SQLITE_SWIFT_STANDALONE +import sqlite3 +#elseif SQLITE_SWIFT_SQLCIPHER +import SQLCipher +#elseif os(Linux) +import CSQLite +#else +import SQLite3 +#endif + +extension Connection { + private typealias Aggregate = @convention(block) (Int, Context, Int32, Argv) -> Void + + /// Creates or redefines a custom SQL aggregate. + /// + /// - Parameters: + /// + /// - aggregate: The name of the aggregate to create or redefine. + /// + /// - argumentCount: The number of arguments that the aggregate takes. If + /// `nil`, the aggregate may take any number of arguments. + /// + /// Default: `nil` + /// + /// - deterministic: Whether or not the aggregate is deterministic (_i.e._ + /// the aggregate always returns the same result for a given input). + /// + /// Default: `false` + /// + /// - step: A block of code to run for each row of an aggregation group. + /// The block is called with an array of raw SQL values mapped to the + /// aggregate’s parameters, and an UnsafeMutablePointer to a state + /// variable. + /// + /// - final: A block of code to run after each row of an aggregation group + /// is processed. The block is called with an UnsafeMutablePointer to a + /// state variable, and should return a raw SQL value (or nil). + /// + /// - state: A block of code to run to produce a fresh state variable for + /// each aggregation group. The block should return an + /// UnsafeMutablePointer to the fresh state variable. + public func createAggregation( + _ functionName: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + step: @escaping ([Binding?], UnsafeMutablePointer) -> Void, + final: @escaping (UnsafeMutablePointer) -> Binding?, + state: @escaping () -> UnsafeMutablePointer) { + + let argc = argumentCount.map { Int($0) } ?? -1 + let box: Aggregate = { (stepFlag: Int, context: Context, argc: Int32, argv: Argv) in + let nBytes = Int32(MemoryLayout>.size) + guard let aggregateContext = sqlite3_aggregate_context(context, nBytes) else { + fatalError("Could not get aggregate context") + } + let mutablePointer = aggregateContext.assumingMemoryBound(to: UnsafeMutableRawPointer.self) + if stepFlag > 0 { + let arguments = argv.getBindings(argc: argc) + if aggregateContext.assumingMemoryBound(to: Int64.self).pointee == 0 { + mutablePointer.pointee = UnsafeMutableRawPointer(mutating: state()) + } + step(arguments, mutablePointer.pointee.assumingMemoryBound(to: T.self)) + } else { + let result = final(mutablePointer.pointee.assumingMemoryBound(to: T.self)) + context.set(result: result) + } + } + + func xStep(context: Context, argc: Int32, value: Argv) { + unsafeBitCast(sqlite3_user_data(context), to: Aggregate.self)(1, context, argc, value) + } + + func xFinal(context: Context) { + unsafeBitCast(sqlite3_user_data(context), to: Aggregate.self)(0, context, 0, nil) + } + + let flags = SQLITE_UTF8 | (deterministic ? SQLITE_DETERMINISTIC : 0) + let resultCode = sqlite3_create_function_v2( + handle, + functionName, + Int32(argc), + flags, + /* pApp */ unsafeBitCast(box, to: UnsafeMutableRawPointer.self), + /* xFunc */ nil, xStep, xFinal, /* xDestroy */ nil + ) + if let result = Result(errorCode: resultCode, connection: self) { + fatalError("Error creating function: \(result)") + } + register(functionName, argc: argc, value: box) + } + + func createAggregation( + _ aggregate: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + initialValue: T, + reduce: @escaping (T, [Binding?]) -> T, + result: @escaping (T) -> Binding? + ) { + let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, ptr) in + let pointer = ptr.pointee.assumingMemoryBound(to: T.self) + let current = Unmanaged.fromOpaque(pointer).takeRetainedValue() + let next = reduce(current, bindings) + ptr.pointee = Unmanaged.passRetained(next).toOpaque() + } + + let final: (UnsafeMutablePointer) -> Binding? = { ptr in + let pointer = ptr.pointee.assumingMemoryBound(to: T.self) + let obj = Unmanaged.fromOpaque(pointer).takeRetainedValue() + let value = result(obj) + ptr.deallocate() + return value + } + + let state: () -> UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee = Unmanaged.passRetained(initialValue).toOpaque() + return pointer + } + + createAggregation(aggregate, step: step, final: final, state: state) + } + + func createAggregation( + _ aggregate: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, + initialValue: T, + reduce: @escaping (T, [Binding?]) -> T, + result: @escaping (T) -> Binding? + ) { + + let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, pointer) in + let current = pointer.pointee + let next = reduce(current, bindings) + pointer.pointee = next + } + + let final: (UnsafeMutablePointer) -> Binding? = { pointer in + let value = result(pointer.pointee) + pointer.deallocate() + return value + } + + let state: () -> UnsafeMutablePointer = { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: initialValue) + return pointer + } + + createAggregation(aggregate, step: step, final: final, state: state) + } + +} diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift index a9012462..6b5481e3 100644 --- a/Sources/SQLite/Core/Connection.swift +++ b/Sources/SQLite/Core/Connection.swift @@ -35,7 +35,6 @@ import SQLite3 #endif /// A connection to SQLite. -// swiftlint:disable:next type_body_length public final class Connection { /// The location of a SQLite database. @@ -586,182 +585,42 @@ public final class Connection { /// - block: A block of code to run when the function is called. The block /// is called with an array of raw SQL values mapped to the function’s /// parameters and should return a raw SQL value (or nil). - // swiftlint:disable:next cyclomatic_complexity - public func createFunction(_ function: String, argumentCount: UInt? = nil, deterministic: Bool = false, + public func createFunction(_ functionName: String, + argumentCount: UInt? = nil, + deterministic: Bool = false, _ block: @escaping (_ args: [Binding?]) -> Binding?) { let argc = argumentCount.map { Int($0) } ?? -1 - let box: Function = { context, argc, argv in - let arguments: [Binding?] = (0..( - _ aggregate: String, - argumentCount: UInt? = nil, - deterministic: Bool = false, - step: @escaping ([Binding?], UnsafeMutablePointer) -> Void, - final: @escaping (UnsafeMutablePointer) -> Binding?, - state: @escaping () -> UnsafeMutablePointer) { - - let argc = argumentCount.map { Int($0) } ?? -1 - let box: Aggregate = { (stepFlag: Int, context: OpaquePointer?, argc: Int32, argv: UnsafeMutablePointer?) in - let ptr = sqlite3_aggregate_context(context, 64)! // needs to be at least as large as uintptr_t; better way to do this? - let mutablePointer = ptr.assumingMemoryBound(to: UnsafeMutableRawPointer.self) - if stepFlag > 0 { - let arguments: [Binding?] = (0..?) -> Void - fileprivate typealias Function = @convention(block) (OpaquePointer?, Int32, UnsafeMutablePointer?) -> Void - fileprivate var functions = [String: [Int: Function]]() - fileprivate var aggregations = [String: [Int: Aggregate]]() + fileprivate typealias Function = @convention(block) (Context, Int32, Argv) -> Void + fileprivate var functions = [String: [Int: Any]]() /// Defines a new collating sequence. /// @@ -808,7 +667,6 @@ public final class Connection { /// Default: `.main`. /// /// - Returns: A new database backup. - public func backup(databaseName: Backup.DatabaseName = .main, usingConnection targetConnection: Connection, andDatabaseName targetDatabaseName: Backup.DatabaseName = .main) throws -> Backup { @@ -864,3 +722,46 @@ extension Connection.Location: CustomStringConvertible { } } + +typealias Context = OpaquePointer? +extension Context { + func set(result: Binding?) { + switch result { + case let blob as Blob: + sqlite3_result_blob(self, blob.bytes, Int32(blob.bytes.count), nil) + case let double as Double: + sqlite3_result_double(self, double) + case let int as Int64: + sqlite3_result_int64(self, int) + case let string as String: + sqlite3_result_text(self, string, Int32(string.lengthOfBytes(using: .utf8)), SQLITE_TRANSIENT) + case .none: + sqlite3_result_null(self) + default: + fatalError("unsupported result type: \(String(describing: result))") + } + } +} + +typealias Argv = UnsafeMutablePointer? +extension Argv { + func getBindings(argc: Int32) -> [Binding?] { + (0..( - _ aggregate: String, - argumentCount: UInt? = nil, - deterministic: Bool = false, - initialValue: T, - reduce: @escaping (T, [Binding?]) -> T, - result: @escaping (T) -> Binding? - ) { - - let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, ptr) in - let pointer = ptr.pointee.assumingMemoryBound(to: T.self) - let current = Unmanaged.fromOpaque(pointer).takeRetainedValue() - let next = reduce(current, bindings) - ptr.pointee = Unmanaged.passRetained(next).toOpaque() - } - - let final: (UnsafeMutablePointer) -> Binding? = { (ptr) in - let pointer = ptr.pointee.assumingMemoryBound(to: T.self) - let obj = Unmanaged.fromOpaque(pointer).takeRetainedValue() - let value = result(obj) - ptr.deallocate() - return value - } - - let state: () -> UnsafeMutablePointer = { - let pointer = UnsafeMutablePointer.allocate(capacity: 1) - pointer.pointee = Unmanaged.passRetained(initialValue).toOpaque() - return pointer - } - - createAggregation(aggregate, step: step, final: final, state: state) - } - - func createAggregation( - _ aggregate: String, - argumentCount: UInt? = nil, - deterministic: Bool = false, - initialValue: T, - reduce: @escaping (T, [Binding?]) -> T, - result: @escaping (T) -> Binding? - ) { - - let step: ([Binding?], UnsafeMutablePointer) -> Void = { (bindings, pointer) in - let current = pointer.pointee - let next = reduce(current, bindings) - pointer.pointee = next - } - - let final: (UnsafeMutablePointer) -> Binding? = { pointer in - let value = result(pointer.pointee) - pointer.deallocate() - return value - } - - let state: () -> UnsafeMutablePointer = { - let pointer = UnsafeMutablePointer.allocate(capacity: 1) - pointer.initialize(to: initialValue) - return pointer - } - - createAggregation(aggregate, step: step, final: final, state: state) - } - } diff --git a/Sources/SQLite/Typed/Query.swift b/Sources/SQLite/Typed/Query.swift index 59b24a8d..2d88db63 100644 --- a/Sources/SQLite/Typed/Query.swift +++ b/Sources/SQLite/Typed/Query.swift @@ -21,7 +21,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // -// swiftlint:disable file_length import Foundation public protocol QueryType: Expressible {