diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index d2ac9df4..6b145ca1 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -46,6 +46,17 @@ 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B331C3F142E00AE3E12 /* ValueTests.swift */; }; 03A65E951C6BB3030062603F /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247B161C3F127200AE3E12 /* TestHelpers.swift */; }; 03A65E971C6BB3210062603F /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E961C6BB3210062603F /* libsqlite3.tbd */; }; + AA780B3D1CC201A700E0E95E /* ConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3B1CC201A700E0E95E /* ConnectionPool.swift */; }; + AA780B3E1CC201A700E0E95E /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3C1CC201A700E0E95E /* Dispatcher.swift */; }; + AA780B411CC202C800E0E95E /* ConnectionPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3F1CC202B000E0E95E /* ConnectionPoolTests.swift */; }; + AA780B421CC202C900E0E95E /* ConnectionPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3F1CC202B000E0E95E /* ConnectionPoolTests.swift */; }; + AA780B431CC202CA00E0E95E /* ConnectionPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3F1CC202B000E0E95E /* ConnectionPoolTests.swift */; }; + AA780B441CC202F300E0E95E /* ConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3B1CC201A700E0E95E /* ConnectionPool.swift */; }; + AA780B451CC202F300E0E95E /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3C1CC201A700E0E95E /* Dispatcher.swift */; }; + AA780B461CC202F400E0E95E /* ConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3B1CC201A700E0E95E /* ConnectionPool.swift */; }; + AA780B471CC202F400E0E95E /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3C1CC201A700E0E95E /* Dispatcher.swift */; }; + AA780B481CC202F500E0E95E /* ConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3B1CC201A700E0E95E /* ConnectionPool.swift */; }; + AA780B491CC202F500E0E95E /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA780B3C1CC201A700E0E95E /* Dispatcher.swift */; }; EE247AD71C3F04ED00AE3E12 /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE247ADE1C3F04ED00AE3E12 /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE247AD31C3F04ED00AE3E12 /* SQLite.framework */; }; EE247B031C3F06E900AE3E12 /* Blob.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AEE1C3F06E900AE3E12 /* Blob.swift */; }; @@ -162,25 +173,28 @@ 39548A6D1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; 39548A6F1CA63C740003E3B5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; A121AC451CA35C79005A31D1 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AA780B3B1CC201A700E0E95E /* ConnectionPool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ConnectionPool.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + AA780B3C1CC201A700E0E95E /* Dispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatcher.swift; sourceTree = ""; }; + AA780B3F1CC202B000E0E95E /* ConnectionPoolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPoolTests.swift; sourceTree = ""; }; EE247AD31C3F04ED00AE3E12 /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AD61C3F04ED00AE3E12 /* SQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SQLite.h; sourceTree = ""; }; EE247AD81C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EE247ADD1C3F04ED00AE3E12 /* SQLiteTests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; EE247AE41C3F04ED00AE3E12 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; EE247AEE1C3F06E900AE3E12 /* Blob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blob.swift; sourceTree = ""; }; - EE247AEF1C3F06E900AE3E12 /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Connection.swift; sourceTree = ""; }; + EE247AEF1C3F06E900AE3E12 /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Connection.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE247AF01C3F06E900AE3E12 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fts3_tokenizer.h; sourceTree = ""; }; EE247AF11C3F06E900AE3E12 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SQLite-Bridging.m"; sourceTree = ""; }; - EE247AF21C3F06E900AE3E12 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Statement.swift; sourceTree = ""; }; + EE247AF21C3F06E900AE3E12 /* Statement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Statement.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE247AF31C3F06E900AE3E12 /* Value.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Value.swift; sourceTree = ""; }; - EE247AF51C3F06E900AE3E12 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FTS4.swift; sourceTree = ""; }; + EE247AF51C3F06E900AE3E12 /* FTS4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FTS4.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE247AF61C3F06E900AE3E12 /* R*Tree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "R*Tree.swift"; sourceTree = ""; }; EE247AF71C3F06E900AE3E12 /* Foundation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Foundation.swift; sourceTree = ""; }; EE247AF81C3F06E900AE3E12 /* Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; EE247AFA1C3F06E900AE3E12 /* AggregateFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctions.swift; sourceTree = ""; }; EE247AFB1C3F06E900AE3E12 /* Collation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collation.swift; sourceTree = ""; }; EE247AFC1C3F06E900AE3E12 /* CoreFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctions.swift; sourceTree = ""; }; - EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctions.swift; sourceTree = ""; }; + EE247AFD1C3F06E900AE3E12 /* CustomFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CustomFunctions.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE247AFE1C3F06E900AE3E12 /* Expression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; EE247AFF1C3F06E900AE3E12 /* Operators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operators.swift; sourceTree = ""; }; EE247B001C3F06E900AE3E12 /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; @@ -190,7 +204,7 @@ EE247B181C3F134A00AE3E12 /* SetterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetterTests.swift; sourceTree = ""; }; EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AggregateFunctionsTests.swift; sourceTree = ""; }; EE247B1B1C3F137700AE3E12 /* BlobTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobTests.swift; sourceTree = ""; }; - EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTests.swift; sourceTree = ""; }; + EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ConnectionTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; EE247B1E1C3F137700AE3E12 /* CoreFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreFunctionsTests.swift; sourceTree = ""; }; EE247B1F1C3F137700AE3E12 /* CustomFunctionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunctionsTests.swift; sourceTree = ""; }; EE247B201C3F137700AE3E12 /* ExpressionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExpressionTests.swift; sourceTree = ""; }; @@ -388,6 +402,7 @@ EE247AE11C3F04ED00AE3E12 /* SQLiteTests */ = { isa = PBXGroup; children = ( + AA780B3F1CC202B000E0E95E /* ConnectionPoolTests.swift */, EE247B1A1C3F137700AE3E12 /* AggregateFunctionsTests.swift */, EE247B1B1C3F137700AE3E12 /* BlobTests.swift */, EE247B1D1C3F137700AE3E12 /* ConnectionTests.swift */, @@ -411,6 +426,8 @@ EE247AED1C3F06E900AE3E12 /* Core */ = { isa = PBXGroup; children = ( + AA780B3B1CC201A700E0E95E /* ConnectionPool.swift */, + AA780B3C1CC201A700E0E95E /* Dispatcher.swift */, EE91808D1C46E5230038162A /* SQLite-Bridging.h */, EE247AEE1C3F06E900AE3E12 /* Blob.swift */, EE247AEF1C3F06E900AE3E12 /* Connection.swift */, @@ -780,9 +797,11 @@ 03A65E741C6BB2DA0062603F /* Helpers.swift in Sources */, 03A65E831C6BB2FB0062603F /* Operators.swift in Sources */, 03A65E851C6BB2FB0062603F /* Schema.swift in Sources */, + AA780B471CC202F400E0E95E /* Dispatcher.swift in Sources */, 03A65E841C6BB2FB0062603F /* Query.swift in Sources */, 03A65E7C1C6BB2F70062603F /* FTS4.swift in Sources */, 03A65E771C6BB2E60062603F /* Connection.swift in Sources */, + AA780B461CC202F400E0E95E /* ConnectionPool.swift in Sources */, 03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -792,6 +811,7 @@ buildActionMask = 2147483647; files = ( 03A65E881C6BB3030062603F /* BlobTests.swift in Sources */, + AA780B431CC202CA00E0E95E /* ConnectionPoolTests.swift in Sources */, 03A65E901C6BB3030062603F /* R*TreeTests.swift in Sources */, 03A65E941C6BB3030062603F /* ValueTests.swift in Sources */, 03A65E8F1C6BB3030062603F /* QueryTests.swift in Sources */, @@ -813,6 +833,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA780B491CC202F500E0E95E /* Dispatcher.swift in Sources */, + AA780B481CC202F500E0E95E /* ConnectionPool.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -834,9 +856,11 @@ EE247B081C3F06E900AE3E12 /* Value.swift in Sources */, EE247B121C3F06E900AE3E12 /* Operators.swift in Sources */, EE247B141C3F06E900AE3E12 /* Schema.swift in Sources */, + AA780B3E1CC201A700E0E95E /* Dispatcher.swift in Sources */, EE247B131C3F06E900AE3E12 /* Query.swift in Sources */, EE247B061C3F06E900AE3E12 /* SQLite-Bridging.m in Sources */, EE247B071C3F06E900AE3E12 /* Statement.swift in Sources */, + AA780B3D1CC201A700E0E95E /* ConnectionPool.swift in Sources */, EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -846,6 +870,7 @@ buildActionMask = 2147483647; files = ( EE247B261C3F137700AE3E12 /* CoreFunctionsTests.swift in Sources */, + AA780B411CC202C800E0E95E /* ConnectionPoolTests.swift in Sources */, EE247B291C3F137700AE3E12 /* FTS4Tests.swift in Sources */, EE247B191C3F134A00AE3E12 /* SetterTests.swift in Sources */, EE247B311C3F141E00AE3E12 /* SchemaTests.swift in Sources */, @@ -881,9 +906,11 @@ EE247B641C3F3FDB00AE3E12 /* Helpers.swift in Sources */, EE247B721C3F3FEC00AE3E12 /* Operators.swift in Sources */, EE247B741C3F3FEC00AE3E12 /* Schema.swift in Sources */, + AA780B451CC202F300E0E95E /* Dispatcher.swift in Sources */, EE247B731C3F3FEC00AE3E12 /* Query.swift in Sources */, EE247B6B1C3F3FEC00AE3E12 /* FTS4.swift in Sources */, EE247B661C3F3FEC00AE3E12 /* Connection.swift in Sources */, + AA780B441CC202F300E0E95E /* ConnectionPool.swift in Sources */, EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -893,6 +920,7 @@ buildActionMask = 2147483647; files = ( EE247B561C3F3FC700AE3E12 /* CoreFunctionsTests.swift in Sources */, + AA780B421CC202C900E0E95E /* ConnectionPoolTests.swift in Sources */, EE247B5A1C3F3FC700AE3E12 /* OperatorsTests.swift in Sources */, EE247B541C3F3FC700AE3E12 /* BlobTests.swift in Sources */, EE247B5D1C3F3FC700AE3E12 /* SchemaTests.swift in Sources */, diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift index 7d67b051..4ae468dd 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -25,8 +25,209 @@ import Dispatch import CSQLite + +/// The mode in which a transaction acquires a lock. +public enum TransactionMode : String { + + /// Defers locking the database till the first read/write executes. + case Deferred = "DEFERRED" + + /// Immediately acquires a reserved lock on the database. + case Immediate = "IMMEDIATE" + + /// Immediately acquires an exclusive lock on all databases. + case Exclusive = "EXCLUSIVE" + +} + + +/// Protocol to an SQLite connection +public protocol Connection { + + /// Whether or not the database was opened in a read-only state. + var readonly : Bool { get } + + /// The last rowid inserted into the database via this connection. + var lastInsertRowid : Int64? { get } + + /// The last number of changes (inserts, updates, or deletes) made to the + /// database via this connection. + var changes : Int { get } + + /// The total number of changes (inserts, updates, or deletes) made to the + /// database via this connection. + var totalChanges : Int { get } + + // MARK: - Execute + + /// Executes a batch of SQL statements. + /// + /// - Parameter SQL: A batch of zero or more semicolon-separated SQL + /// statements. + /// + /// - Throws: `Result.Error` if query execution fails. + func execute(SQL: String) throws + + // MARK: - Prepare + + /// Prepares a single SQL statement (with optional parameter bindings). + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + @warn_unused_result func prepare(statement: String, _ bindings: Binding?...) throws -> Statement + + /// Prepares a single SQL statement and binds parameters to it. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + @warn_unused_result func prepare(statement: String, _ bindings: [Binding?]) throws -> Statement + + /// Prepares a single SQL statement and binds parameters to it. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Returns: A prepared statement. + @warn_unused_result func prepare(statement: String, _ bindings: [String: Binding?]) throws -> Statement + + // MARK: - Run + + /// Runs a single SQL statement (with optional parameter bindings). + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + func run(statement: String, _ bindings: Binding?...) throws -> Statement + + /// Prepares, binds, and runs a single SQL statement. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + func run(statement: String, _ bindings: [Binding?]) throws -> Statement + + /// Prepares, binds, and runs a single SQL statement. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Throws: `Result.Error` if query execution fails. + /// + /// - Returns: The statement. + func run(statement: String, _ bindings: [String: Binding?]) throws -> Statement + + // MARK: - Scalar + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + @warn_unused_result func scalar(statement: String, _ bindings: Binding?...) -> Binding? + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A list of parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + @warn_unused_result func scalar(statement: String, _ bindings: [Binding?]) -> Binding? + + /// Runs a single SQL statement (with optional parameter bindings), + /// returning the first value of the first row. + /// + /// - Parameters: + /// + /// - statement: A single SQL statement. + /// + /// - bindings: A dictionary of named parameters to bind to the statement. + /// + /// - Returns: The first value of the first row returned. + @warn_unused_result func scalar(statement: String, _ bindings: [String: Binding?]) -> Binding? + + // MARK: - Transactions + + // TODO: Consider not requiring a throw to roll back? + /// Runs a transaction with the given mode. + /// + /// - Note: Transactions cannot be nested. To nest transactions, see + /// `savepoint()`, instead. + /// + /// - Parameters: + /// + /// - mode: The mode in which a transaction acquires a lock. + /// + /// Default: `.Deferred` + /// + /// - block: A closure to run SQL statements within the transaction. + /// The transaction will be committed when the block returns. The block + /// must throw to roll the transaction back. + /// + /// - Throws: `Result.Error`, and rethrows. + func transaction(mode: TransactionMode, block: (Connection) throws -> Void) throws + + // TODO: Consider not requiring a throw to roll back? + // TODO: Consider removing ability to set a name? + /// Runs a transaction with the given savepoint name (if omitted, it will + /// generate a UUID). + /// + /// - SeeAlso: `transaction()`. + /// + /// - Parameters: + /// + /// - savepointName: A unique identifier for the savepoint (optional). + /// + /// - block: A closure to run SQL statements within the transaction. + /// The savepoint will be released (committed) when the block returns. + /// The block must throw to roll the savepoint back. + /// + /// - Throws: `SQLite.Result.Error`, and rethrows. + func savepoint(name: String, block: (Connection) throws -> Void) throws + + func sync(block: () throws -> T) rethrows -> T + +} + + /// A connection to SQLite. -public final class Connection { +public final class DirectConnection : Connection, Equatable { /// The location of a SQLite database. public enum Location { @@ -67,10 +268,32 @@ public final class Connection { /// Default: `false`. /// /// - Returns: A new database connection. - public init(_ location: Location = .InMemory, readonly: Bool = false) throws { + public convenience init(_ location: Location = .InMemory, readonly: Bool = false, vfsName: String? = nil) throws { let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE - try check(sqlite3_open_v2(location.description, &_handle, flags | SQLITE_OPEN_FULLMUTEX, nil)) - dispatch_queue_set_specific(queue, Connection.queueKey, queueContext, nil) + try self.init(location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.Connection"), vfsName: vfsName) + } + + /// Initializes a new SQLite connection. + /// + /// - Parameters: + /// + /// - location: The location of the database. Creates a new database if it + /// doesn’t already exist (unless in read-only mode). + /// + /// - flags: SQLite open flags + /// + /// - dispatcher: Dispatcher synchronization blocks + /// + /// - Returns: A new database connection. + public init(_ location: Location, flags: Int32, dispatcher: Dispatcher, vfsName: String? = nil) throws { + self.dispatcher = dispatcher + if let vfsName = vfsName { + try check(sqlite3_open_v2(location.description, &_handle, flags, vfsName)) + } + else { + try check(sqlite3_open_v2(location.description, &_handle, flags, nil)) + } + try check(sqlite3_extended_result_codes(handle, 1)) } /// Initializes a new connection to a database. @@ -265,20 +488,6 @@ public final class Connection { // MARK: - Transactions - /// The mode in which a transaction acquires a lock. - public enum TransactionMode : String { - - /// Defers locking the database till the first read/write executes. - case Deferred = "DEFERRED" - - /// Immediately acquires a reserved lock on the database. - case Immediate = "IMMEDIATE" - - /// Immediately acquires an exclusive lock on all databases. - case Exclusive = "EXCLUSIVE" - - } - // TODO: Consider not requiring a throw to roll back? /// Runs a transaction with the given mode. /// @@ -296,10 +505,10 @@ public final class Connection { /// must throw to roll the transaction back. /// /// - Throws: `Result.Error`, and rethrows. - public func transaction(mode: TransactionMode = .Deferred, block: () throws -> Void) throws { + public func transaction(mode: TransactionMode = .Deferred, block: (Connection) throws -> Void) throws { try transaction("BEGIN \(mode.rawValue) TRANSACTION", block, "COMMIT TRANSACTION", or: "ROLLBACK TRANSACTION") } - + // TODO: Consider not requiring a throw to roll back? // TODO: Consider removing ability to set a name? /// Runs a transaction with the given savepoint name (if omitted, it will @@ -316,18 +525,18 @@ public final class Connection { /// The block must throw to roll the savepoint back. /// /// - Throws: `SQLite.Result.Error`, and rethrows. - public func savepoint(name: String = NSUUID().UUIDString, block: () throws -> Void) throws { + public func savepoint(name: String = NSUUID().UUIDString, block: (Connection) throws -> Void) throws { let name = name.quote("'") let savepoint = "SAVEPOINT \(name)" - + try transaction(savepoint, block, "RELEASE \(savepoint)", or: "ROLLBACK TO \(savepoint)") } - - private func transaction(begin: String, _ block: () throws -> Void, _ commit: String, or rollback: String) throws { + + private func transaction(begin: String, _ block: (Connection) throws -> Void, _ commit: String, or rollback: String) throws { return try sync { try self.run(begin) do { - try block() + try block(self) } catch { try self.run(rollback) throw error @@ -335,7 +544,7 @@ public final class Connection { try self.run(commit) } } - + /// Interrupts any long-running queries. public func interrupt() { sqlite3_interrupt(handle) @@ -565,7 +774,7 @@ public final class Connection { // MARK: - Error Handling - func sync(block: () throws -> T) rethrows -> T { + public func sync(block: () throws -> T) rethrows -> T { var success: T? var failure: ErrorType? @@ -577,11 +786,7 @@ public final class Connection { } } - if dispatch_get_specific(Connection.queueKey) == queueContext { - box() - } else { - dispatch_sync(queue, box) // FIXME: rdar://problem/21389236 - } + dispatcher.dispatch(box) if let failure = failure { try { () -> Void in throw failure }() @@ -597,16 +802,12 @@ public final class Connection { throw error } - - private var queue = dispatch_queue_create("SQLite.Database", DISPATCH_QUEUE_SERIAL) - - private static let queueKey = unsafeBitCast(Connection.self, UnsafePointer.self) - - private lazy var queueContext: UnsafeMutablePointer = unsafeBitCast(self, UnsafeMutablePointer.self) + + private var dispatcher : Dispatcher } -extension Connection : CustomStringConvertible { +extension DirectConnection : CustomStringConvertible { public var description: String { return String.fromCString(sqlite3_db_filename(handle, nil))! @@ -614,7 +815,7 @@ extension Connection : CustomStringConvertible { } -extension Connection.Location : CustomStringConvertible { +extension DirectConnection.Location : CustomStringConvertible { public var description: String { switch self { @@ -629,6 +830,10 @@ extension Connection.Location : CustomStringConvertible { } +public func ==(lhs: DirectConnection, rhs: DirectConnection) -> Bool { + return lhs === rhs +} + /// An SQL operation passed to update callbacks. public enum Operation { @@ -662,7 +867,7 @@ public enum Result : ErrorType { case Error(message: String, code: Int32, statement: Statement?) - init?(errorCode: Int32, connection: Connection, statement: Statement? = nil) { + init?(errorCode: Int32, connection: DirectConnection, statement: Statement? = nil) { guard !Result.successCodes.contains(errorCode) else { return nil } let message = String.fromCString(sqlite3_errmsg(connection.handle))! diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift new file mode 100644 index 00000000..dd644304 --- /dev/null +++ b/SQLite/Core/ConnectionPool.swift @@ -0,0 +1,201 @@ +// +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Dispatch +import CSQLite + + +private let vfsName = "unix-excl" + + +// Connection pool for accessing an SQLite database +// with multiple readers & a single writer. Utilizes +// WAL mode. +public final class ConnectionPool { + + private let location : DirectConnection.Location + private var availableReadConnections = [DirectConnection]() + private var unavailableReadConnections = [DirectConnection]() + private let lockQueue : dispatch_queue_t + private var writeConnection : DirectConnection! + + public var foreignKeys : Bool { + get { + return internalSetup[.ForeignKeys] != nil + } + set { + internalSetup[.ForeignKeys] = newValue ? { try $0.execute("PRAGMA foreign_keys = ON;") } : nil + } + } + + public typealias ConnectionProcessor = Connection throws -> Void + public var setup = [ConnectionProcessor]() + + private enum InternalOption { + case WriteAheadLogging + case ForeignKeys + } + + private var internalSetup = [InternalOption: ConnectionProcessor]() + + public init(_ location: DirectConnection.Location) throws { + self.location = location + self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool.Lock", DISPATCH_QUEUE_SERIAL) + self.internalSetup[.WriteAheadLogging] = { try $0.execute("PRAGMA journal_mode = WAL;") } + } + + public var totalReadableConnectionCount : Int { + return availableReadConnections.count + unavailableReadConnections.count + } + + public var availableReadableConnectionCount : Int { + return availableReadConnections.count + } + + // Connection that automatically returns itself + // to the pool when it goes out of scope + private class BorrowedConnection : Connection, Equatable { + + let pool : ConnectionPool + let connection : DirectConnection + + init(pool: ConnectionPool, connection: DirectConnection) { + self.pool = pool + self.connection = connection + } + + deinit { + dispatch_sync(pool.lockQueue) { + if let index = self.pool.unavailableReadConnections.indexOf(self.connection) { + self.pool.unavailableReadConnections.removeAtIndex(index) + } + self.pool.availableReadConnections.append(self.connection) + } + } + + var readonly : Bool { return connection.readonly } + var lastInsertRowid : Int64? { return connection.lastInsertRowid } + var changes : Int { return connection.changes } + var totalChanges : Int { return connection.totalChanges } + + func execute(SQL: String) throws { return try connection.execute(SQL) } + @warn_unused_result func prepare(statement: String, _ bindings: Binding?...) throws -> Statement { return try connection.prepare(statement, bindings) } + @warn_unused_result func prepare(statement: String, _ bindings: [Binding?]) throws -> Statement { return try connection.prepare(statement, bindings) } + @warn_unused_result func prepare(statement: String, _ bindings: [String: Binding?]) throws -> Statement { return try connection.prepare(statement, bindings) } + + func run(statement: String, _ bindings: Binding?...) throws -> Statement { return try connection.run(statement, bindings) } + func run(statement: String, _ bindings: [Binding?]) throws -> Statement { return try connection.run(statement, bindings) } + func run(statement: String, _ bindings: [String: Binding?]) throws -> Statement { return try connection.run(statement, bindings) } + + @warn_unused_result func scalar(statement: String, _ bindings: Binding?...) -> Binding? { return connection.scalar(statement, bindings) } + @warn_unused_result func scalar(statement: String, _ bindings: [Binding?]) -> Binding? { return connection.scalar(statement, bindings) } + @warn_unused_result func scalar(statement: String, _ bindings: [String: Binding?]) -> Binding? { return connection.scalar(statement, bindings) } + + func transaction(mode: TransactionMode, block: (Connection) throws -> Void) throws { return try connection.transaction(mode, block: block) } + func savepoint(name: String, block: (Connection) throws -> Void) throws { return try connection.savepoint(name, block: block) } + + func sync(block: () throws -> T) rethrows -> T { return try connection.sync(block) } + func check(resultCode: Int32, statement: Statement? = nil) throws -> Int32 { return try connection.check(resultCode, statement: statement) } + + } + + + // Acquires a read/write connection to the database + + var writeConnectionInit = dispatch_once_t() + + public var writable : DirectConnection { + + dispatch_once(&writeConnectionInit) { + + let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX + self.writeConnection = try! DirectConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.ConnectionPool.Write"), vfsName: vfsName) + self.writeConnection.busyTimeout = 2 + + for setupProcessor in self.internalSetup.values { + try! setupProcessor(self.writeConnection) + } + + for setupProcessor in self.setup { + try! setupProcessor(self.writeConnection) + } + + } + + return writeConnection + } + + // Acquires a read only connection to the database + public var readable : Connection { + + var borrowed : BorrowedConnection! + + repeat { + + dispatch_sync(lockQueue) { + + // Ensure database is open + self.writable + + let connection : DirectConnection + + if let availableConnection = self.availableReadConnections.popLast() { + connection = availableConnection + } + else { + + let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX + + connection = try! DirectConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher(), vfsName: vfsName) + connection.busyTimeout = 2 + + for (type, setupProcessor) in self.internalSetup { + if type == .WriteAheadLogging { + continue + } + try! setupProcessor(connection) + } + + for setupProcessor in self.setup { + try! setupProcessor(connection) + } + + } + + self.unavailableReadConnections.append(connection) + + borrowed = BorrowedConnection(pool: self, connection: connection) + } + + } while borrowed == nil + + return borrowed + } + +} + + +private func ==(lhs: ConnectionPool.BorrowedConnection, rhs: ConnectionPool.BorrowedConnection) -> Bool { + return lhs.connection == rhs.connection +} diff --git a/SQLite/Core/Dispatcher.swift b/SQLite/Core/Dispatcher.swift new file mode 100644 index 00000000..b5010488 --- /dev/null +++ b/SQLite/Core/Dispatcher.swift @@ -0,0 +1,55 @@ +// +// Dispatcher.swift +// SQLite +// +// Created by Kevin Wooten on 11/28/15. +// Copyright © 2015 stephencelis. All rights reserved. +// + +import Foundation + + +/// Block dispatch method +public protocol Dispatcher { + + /// Dispatches the provided block + func dispatch(block: dispatch_block_t) + +} + + +/// Dispatches block immediately on current thread +public final class ImmediateDispatcher : Dispatcher { + + public func dispatch(block: dispatch_block_t) { + block() + } + +} + + +/// Synchronously dispatches block on a serial +/// queue. Specifically allows reentrant calls +public final class ReentrantDispatcher : Dispatcher { + + static let queueKey = unsafeBitCast(ReentrantDispatcher.self, UnsafePointer.self) + + let queue : dispatch_queue_t + + let queueContext : UnsafeMutablePointer! + + public init(_ name: String) { + queue = dispatch_queue_create(name, DISPATCH_QUEUE_SERIAL) + queueContext = unsafeBitCast(queue, UnsafeMutablePointer.self) + dispatch_queue_set_specific(queue, ReentrantDispatcher.queueKey, queueContext, nil) + } + + public func dispatch(block: dispatch_block_t) { + if dispatch_get_specific(ReentrantDispatcher.queueKey) == self.queueContext { + block() + } else { + dispatch_sync(self.queue, block) // FIXME: rdar://problem/21389236 + } + } + +} diff --git a/SQLite/Core/Statement.swift b/SQLite/Core/Statement.swift index 39fb000d..d7652c1a 100644 --- a/SQLite/Core/Statement.swift +++ b/SQLite/Core/Statement.swift @@ -29,9 +29,9 @@ public final class Statement { private var handle: COpaquePointer = nil - private let connection: Connection + private let connection: DirectConnection - init(_ connection: Connection, _ SQL: String) throws { + init(_ connection: DirectConnection, _ SQL: String) throws { self.connection = connection try connection.check(sqlite3_prepare_v2(connection.handle, SQL, -1, &handle, nil)) } diff --git a/SQLite/Extensions/FTS4.swift b/SQLite/Extensions/FTS4.swift index e5a1d815..045e5303 100644 --- a/SQLite/Extensions/FTS4.swift +++ b/SQLite/Extensions/FTS4.swift @@ -140,7 +140,7 @@ extension Tokenizer : CustomStringConvertible { } -extension Connection { +extension DirectConnection { public func registerTokenizer(submoduleName: String, next: String -> (String, Range)?) throws { try check(_SQLiteRegisterTokenizer(handle, Tokenizer.moduleName, submoduleName) { input, offset, length in diff --git a/SQLite/Typed/CustomFunctions.swift b/SQLite/Typed/CustomFunctions.swift index 068d0340..d004f75e 100644 --- a/SQLite/Typed/CustomFunctions.swift +++ b/SQLite/Typed/CustomFunctions.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public extension Connection { +public extension DirectConnection { /// Creates or redefines a custom SQL function. /// diff --git a/SQLite/Typed/Query.swift b/SQLite/Typed/Query.swift index e45034ca..9f966250 100644 --- a/SQLite/Typed/Query.swift +++ b/SQLite/Typed/Query.swift @@ -1051,7 +1051,7 @@ public struct Row { } // FIXME: rdar://problem/18673897 // subscript… - + public subscript(column: Expression) -> Blob { return get(column) } diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift new file mode 100644 index 00000000..74351527 --- /dev/null +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -0,0 +1,133 @@ +import XCTest +import SQLite + +class ConnectionPoolTests : SQLiteTestCase { + + var pool : ConnectionPool! + + override func setUp() { + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") + pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) + } + + func testConnectionSetupClosures() { + + pool.foreignKeys = true + pool.setup.append { try $0.execute("CREATE TABLE IF NOT EXISTS test(value INT)") } + + XCTAssertTrue(pool.readable.scalar("PRAGMA foreign_keys") as! Int64 == 1) + try! pool.writable.execute("INSERT INTO test(value) VALUES (1)") + try! pool.readable.execute("SELECT value FROM test") + } + + func testConcurrentAccess2() { + + let conn = pool.writable + try! conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(id INTEGER PRIMARY KEY, name TEXT);") + try! conn.execute("DELETE FROM test") + try! conn.execute("INSERT INTO test(id,name) VALUES(0, 'test0')") + try! conn.execute("INSERT INTO test(id,name) VALUES(1, 'test1')") + try! conn.execute("INSERT INTO test(id,name) VALUES(2, 'test2')") + try! conn.execute("INSERT INTO test(id,name) VALUES(3, 'test3')") + try! conn.execute("INSERT INTO test(id,name) VALUES(4, 'test4')") + + var quit = false + let queue = dispatch_queue_create("Readers", DISPATCH_QUEUE_CONCURRENT) + for x in 0..<5 { + var reads = 0 + + let ex = expectationWithDescription("thread" + String(x)) + + dispatch_async(queue) { + + print("started", x) + + let conn = self.pool.readable + + let stmt = try! conn.prepare("SELECT name FROM test WHERE id = ?") + var curr = stmt.scalar(x) as! String + while !quit { + + let now = stmt.scalar(x) as! String + if now != curr { + //print(now) + curr = now + } + reads += 1 + } + + print("ended at", reads, "reads") + + ex.fulfill() + } + + } + + for x in 10..<5000 { + + let name = "test" + String(x) + let idx = Int(rand()) % 5 + + do { + try conn.run("UPDATE test SET name=? WHERE id=?", name, idx) + } + catch let error { + XCTFail((error as? CustomStringConvertible)?.description ?? "Unknown") + } + + usleep(500) + } + + quit = true + waitForExpectationsWithTimeout(1000, handler: nil) + } + + func testConcurrentAccess() throws { + + try! pool.writable.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") + try! pool.writable.run("INSERT INTO test(value) VALUES(?)", 0) + + let q = dispatch_queue_create("Readers/Writers", DISPATCH_QUEUE_CONCURRENT); + var finished = false + + for _ in 0..<5 { + + dispatch_async(q) { + + while !finished { + + let val = self.pool.readable.scalar("SELECT value FROM test") + assert(val != nil, "DB query returned nil result set") + + } + + } + + } + + for c in 0..<5000 { + + try pool.writable.run("INSERT INTO test(value) VALUES(?)", c) + + usleep(100); + + } + + finished = true + + // Wait for readers to finish + dispatch_barrier_sync(q) { + } + + } + + func testAutoRelease() { + + do { + try! pool.readable.execute("SELECT 1") + } + + XCTAssertEqual(pool.totalReadableConnectionCount, pool.availableReadableConnectionCount) + } + +} diff --git a/SQLiteTests/ConnectionTests.swift b/SQLiteTests/ConnectionTests.swift index aeec9b72..bbad19a8 100644 --- a/SQLiteTests/ConnectionTests.swift +++ b/SQLiteTests/ConnectionTests.swift @@ -10,27 +10,27 @@ class ConnectionTests : SQLiteTestCase { } func test_init_withInMemory_returnsInMemoryConnection() { - let db = try! Connection(.InMemory) + let db = try! DirectConnection(.InMemory) XCTAssertEqual("", db.description) } func test_init_returnsInMemoryByDefault() { - let db = try! Connection() + let db = try! DirectConnection() XCTAssertEqual("", db.description) } func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! Connection(.Temporary) + let db = try! DirectConnection(.Temporary) XCTAssertEqual("", db.description) } func test_init_withURI_returnsURIConnection() { - let db = try! Connection(.URI("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + let db = try! DirectConnection(.URI("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) } func test_init_withString_returnsURIConnection() { - let db = try! Connection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") + let db = try! DirectConnection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) } @@ -39,7 +39,7 @@ class ConnectionTests : SQLiteTestCase { } func test_readonly_returnsTrueOnReadOnlyConnections() { - let db = try! Connection(readonly: true) + let db = try! DirectConnection(readonly: true) XCTAssertTrue(db.readonly) } @@ -95,19 +95,19 @@ class ConnectionTests : SQLiteTestCase { } func test_transaction_executesBeginDeferred() { - try! db.transaction(.Deferred) {} + try! db.transaction(.Deferred) {_ in } AssertSQL("BEGIN DEFERRED TRANSACTION") } func test_transaction_executesBeginImmediate() { - try! db.transaction(.Immediate) {} + try! db.transaction(.Immediate) {_ in } AssertSQL("BEGIN IMMEDIATE TRANSACTION") } func test_transaction_executesBeginExclusive() { - try! db.transaction(.Exclusive) {} + try! db.transaction(.Exclusive) {_ in } AssertSQL("BEGIN EXCLUSIVE TRANSACTION") } @@ -115,7 +115,7 @@ class ConnectionTests : SQLiteTestCase { func test_transaction_beginsAndCommitsTransactions() { let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") - try! db.transaction { + try! db.transaction {_ in try stmt.run() } @@ -129,7 +129,7 @@ class ConnectionTests : SQLiteTestCase { let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { - try db.transaction { + try db.transaction {_ in try stmt.run() try stmt.run() } @@ -145,8 +145,8 @@ class ConnectionTests : SQLiteTestCase { func test_savepoint_beginsAndCommitsSavepoints() { let db = self.db - try! db.savepoint("1") { - try db.savepoint("2") { + try! db.savepoint("1") {_ in + try db.savepoint("2") {_ in try db.run("INSERT INTO users (email) VALUES (?)", "alice@example.com") } } @@ -165,13 +165,13 @@ class ConnectionTests : SQLiteTestCase { let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com") do { - try db.savepoint("1") { - try db.savepoint("2") { + try db.savepoint("1") {_ in + try db.savepoint("2") {_ in try stmt.run() try stmt.run() try stmt.run() } - try db.savepoint("2") { + try db.savepoint("2") {_ in try stmt.run() try stmt.run() try stmt.run() @@ -235,7 +235,7 @@ class ConnectionTests : SQLiteTestCase { db.commitHook { done() } - try! db.transaction { + try! db.transaction {_ in try self.InsertUser("alice") } XCTAssertEqual(1, db.scalar("SELECT count(*) FROM users") as? Int64) @@ -246,7 +246,7 @@ class ConnectionTests : SQLiteTestCase { async { done in db.rollbackHook(done) do { - try db.transaction { + try db.transaction {_ in try self.InsertUser("alice") try self.InsertUser("alice") // throw } @@ -263,7 +263,7 @@ class ConnectionTests : SQLiteTestCase { } db.rollbackHook(done) do { - try db.transaction { + try db.transaction {_ in try self.InsertUser("alice") } } catch { diff --git a/SQLiteTests/TestHelpers.swift b/SQLiteTests/TestHelpers.swift index 464b9c27..855bf7bd 100644 --- a/SQLiteTests/TestHelpers.swift +++ b/SQLiteTests/TestHelpers.swift @@ -5,7 +5,7 @@ class SQLiteTestCase : XCTestCase { var trace = [String: Int]() - let db = try! Connection() + let db = try! DirectConnection() let users = Table("users")