From 68815609ab1173ec36e4978b2a060ff7c0a8a013 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Sun, 29 Nov 2015 01:54:06 -0700 Subject: [PATCH 01/19] Add connection pool for concurrent access Uses WAL mode to support multiple reads and a single writer. --- SQLite.xcodeproj/project.pbxproj | 28 +++ SQLite/Core/Connection.swift | 256 +++++++++++++++++++++++--- SQLite/Core/ConnectionPool.swift | 144 +++++++++++++++ SQLite/Core/Dispatcher.swift | 55 ++++++ SQLiteTests/ConnectionPoolTests.swift | 57 ++++++ 5 files changed, 511 insertions(+), 29 deletions(-) create mode 100644 SQLite/Core/ConnectionPool.swift create mode 100644 SQLite/Core/Dispatcher.swift create mode 100644 SQLiteTests/ConnectionPoolTests.swift diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index d2ac9df4..edc79e3f 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,6 +173,9 @@ 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; path = ConnectionPool.swift; sourceTree = ""; }; + 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 = ""; }; @@ -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..1b7b3d59 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -25,8 +25,207 @@ 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 ConnectionType { + + /// 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: () 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: () throws -> Void) throws + +} + + /// A connection to SQLite. -public final class Connection { +public final class Connection : ConnectionType, Equatable { /// The location of a SQLite database. public enum Location { @@ -67,10 +266,27 @@ 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) 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")) + } + + /// 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) throws { + self.dispatcher = dispatcher + 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 +481,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. /// @@ -577,11 +779,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,12 +795,8 @@ 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 } @@ -629,6 +823,10 @@ extension Connection.Location : CustomStringConvertible { } +public func ==(lhs: Connection, rhs: Connection) -> Bool { + return lhs === rhs +} + /// An SQL operation passed to update callbacks. public enum Operation { diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift new file mode 100644 index 00000000..5a6ee5b9 --- /dev/null +++ b/SQLite/Core/ConnectionPool.swift @@ -0,0 +1,144 @@ +// +// 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 Foundation + + +/// Connection pool delegate +public protocol ConnectionPoolDelegate { + + func pool(pool: ConnectionPool, shouldAddConnection: Connection) + func pool(pool: ConnectionPool, didAddConnection: Connection) + +} + + +// Connection pool for accessing an SQLite database +// with multiple readers & a single writer. Utilizes +// WAL mode. +public final class ConnectionPool { + + private let location : Connection.Location + private var availableReadConnections = [Connection]() + private var unavailableReadConnections = [Connection]() + private let lockQueue : dispatch_queue_t + private var writeConnection : Connection! + + public init(_ location: Connection.Location) throws { + self.location = location + self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool", DISPATCH_QUEUE_SERIAL) + + try writable.execute("PRAGMA locking_mode = EXCLUSIVE; PRAGMA journal_mode = WAL;") + } + + // Connection that automatically returns itself + // to the pool when it goes out of scope + private class BorrowedConnection : ConnectionType, Equatable { + + let pool : ConnectionPool + let connection : Connection + + init(pool: ConnectionPool, connection: Connection) { + 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: () throws -> Void) throws { return try connection.transaction(mode, block: block) } + func savepoint(name: String, block: () throws -> Void) throws { return try connection.savepoint(name, block: block) } + + } + + + // Acquires a read/write connection to the database + public var writable : Connection { + + var writeConnectionInit = dispatch_once_t() + dispatch_once(&writeConnectionInit) { + let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL + self.writeConnection = try! Connection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.WriteConnection")) + self.writeConnection.busyTimeout = 2 + } + + return writeConnection + } + + // Acquires a read only connection to the database + public var readable : ConnectionType { + + var borrowed : BorrowedConnection! + + dispatch_sync(lockQueue) { + + let connection : Connection + + if let availableConnection = self.availableReadConnections.popLast() { + connection = availableConnection + } + else { + let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL + connection = try! Connection(self.location, flags: flags, dispatcher: ImmediateDispatcher()) + connection.busyTimeout = 2 + } + + self.unavailableReadConnections.append(connection) + + borrowed = BorrowedConnection(pool: self, connection: connection) + } + + 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/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift new file mode 100644 index 00000000..e79d3a8c --- /dev/null +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -0,0 +1,57 @@ +import XCTest +import SQLite + +class ConnectionPoolTests : SQLiteTestCase { + + override func setUp() { + super.setUp() + } + + func testConcurrentAccess() { + + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite") + let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite")) + + let conn = pool.writable + try! conn.execute("CREATE TABLE IF NOT EXISTS 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 = pool.readable + var curr = conn.scalar("SELECT name FROM test WHERE id = ?", x) as! String + while !quit { + let now = conn.scalar("SELECT name FROM test WHERE id = ?", x) as! String + if now != curr { + print(now) + curr = now + } + reads += 1 + } + print("ended at", reads, "reads") + ex.fulfill() + } + } + + for x in 10..<500 { + let name = "test" + String(x) + let idx = Int(rand()) % 5 + try! conn.run("UPDATE test SET name=? WHERE id=?", name, idx) + usleep(15000) + } + + quit = true + waitForExpectationsWithTimeout(1000, handler: nil) + } + +} From 42ebd18a06e06319933ce866dd9fbc84faf2e03a Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Sun, 29 Nov 2015 03:06:14 -0700 Subject: [PATCH 02/19] Add delegate to connection pool Allows customizing new connections once created --- SQLite/Core/ConnectionPool.swift | 53 +++++++++++++++++++-------- SQLiteTests/ConnectionPoolTests.swift | 38 +++++++++++++++++-- 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index 5a6ee5b9..d4a0c3f5 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -28,7 +28,7 @@ import Foundation /// Connection pool delegate public protocol ConnectionPoolDelegate { - func pool(pool: ConnectionPool, shouldAddConnection: Connection) + func poolShouldAddConnection(pool: ConnectionPool) -> Bool func pool(pool: ConnectionPool, didAddConnection: Connection) } @@ -45,6 +45,8 @@ public final class ConnectionPool { private let lockQueue : dispatch_queue_t private var writeConnection : Connection! + public var delegate : ConnectionPoolDelegate? + public init(_ location: Connection.Location) throws { self.location = location self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool", DISPATCH_QUEUE_SERIAL) @@ -52,6 +54,14 @@ public final class ConnectionPool { try writable.execute("PRAGMA locking_mode = EXCLUSIVE; 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 : ConnectionType, Equatable { @@ -98,7 +108,7 @@ public final class ConnectionPool { // Acquires a read/write connection to the database - public var writable : Connection { + public var writable : ConnectionType { var writeConnectionInit = dispatch_once_t() dispatch_once(&writeConnectionInit) { @@ -115,23 +125,34 @@ public final class ConnectionPool { var borrowed : BorrowedConnection! - dispatch_sync(lockQueue) { - - let connection : Connection + repeat { - if let availableConnection = self.availableReadConnections.popLast() { - connection = availableConnection - } - else { - let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL - connection = try! Connection(self.location, flags: flags, dispatcher: ImmediateDispatcher()) - connection.busyTimeout = 2 + dispatch_sync(lockQueue) { + + let connection : Connection + + if let availableConnection = self.availableReadConnections.popLast() { + connection = availableConnection + } + else if self.delegate?.poolShouldAddConnection(self) ?? true { + + let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL + connection = try! Connection(self.location, flags: flags, dispatcher: ImmediateDispatcher()) + connection.busyTimeout = 2 + + self.delegate?.pool(self, didAddConnection: connection) + + } + else { + return + } + + self.unavailableReadConnections.append(connection) + + borrowed = BorrowedConnection(pool: self, connection: connection) } - self.unavailableReadConnections.append(connection) - - borrowed = BorrowedConnection(pool: self, connection: connection) - } + } while borrowed == nil return borrowed } diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index e79d3a8c..12e6402f 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -9,7 +9,7 @@ class ConnectionPoolTests : SQLiteTestCase { func testConcurrentAccess() { - let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite") + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite")) let conn = pool.writable @@ -25,33 +25,63 @@ class ConnectionPoolTests : SQLiteTestCase { 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 = pool.readable - var curr = conn.scalar("SELECT name FROM test WHERE id = ?", x) as! String + + let stmt = conn.prepare("SELECT name FROM test WHERE id = ?") + var curr = stmt.scalar(x) as! String while !quit { - let now = conn.scalar("SELECT name FROM test WHERE id = ?", x) as! String + + 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..<500 { + let name = "test" + String(x) let idx = Int(rand()) % 5 - try! conn.run("UPDATE test SET name=? WHERE id=?", name, idx) + + do { + try conn.run("UPDATE test SET name=? WHERE id=?", name, idx) + } + catch let error { + XCTFail((error as? CustomStringConvertible)?.description ?? "Unknown") + } + usleep(15000) } quit = true waitForExpectationsWithTimeout(1000, handler: nil) } + + func testAutoRelease() { + + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") + let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite")) + + do { + try! pool.readable.execute("SELECT 1") + } + + XCTAssertEqual(pool.totalReadableConnectionCount, pool.availableReadableConnectionCount) + } } From 68f46d5069e0e146055103c6501cc36369181484 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Fri, 15 Apr 2016 21:46:45 -0700 Subject: [PATCH 03/19] Better names for protocols and classes ConnectionType is now Connection and Connection is now DBConnection --- SQLite/Core/Connection.swift | 38 +++++++++++++------------- SQLite/Core/ConnectionPool.swift | 39 +++++++++++++++------------ SQLite/Core/Statement.swift | 4 +-- SQLite/Extensions/FTS4.swift | 2 +- SQLite/Typed/CustomFunctions.swift | 2 +- SQLite/Typed/Query.swift | 2 +- SQLiteTests/ConnectionPoolTests.swift | 8 +++--- SQLiteTests/ConnectionTests.swift | 38 +++++++++++++------------- SQLiteTests/TestHelpers.swift | 2 +- 9 files changed, 71 insertions(+), 64 deletions(-) diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift index 1b7b3d59..e1b82b76 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -42,7 +42,7 @@ public enum TransactionMode : String { /// Protocol to an SQLite connection -public protocol ConnectionType { +public protocol Connection { /// Whether or not the database was opened in a read-only state. var readonly : Bool { get } @@ -201,8 +201,8 @@ public protocol ConnectionType { /// must throw to roll the transaction back. /// /// - Throws: `Result.Error`, and rethrows. - func transaction(mode: TransactionMode, block: () throws -> Void) throws - + 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 @@ -219,13 +219,15 @@ public protocol ConnectionType { /// The block must throw to roll the savepoint back. /// /// - Throws: `SQLite.Result.Error`, and rethrows. - func savepoint(name: String, block: () throws -> Void) throws + func savepoint(name: String, block: (Connection) throws -> Void) throws + func sync(block: () throws -> T) rethrows -> T + } /// A connection to SQLite. -public final class Connection : ConnectionType, Equatable { +public final class DBConnection : Connection, Equatable { /// The location of a SQLite database. public enum Location { @@ -498,10 +500,10 @@ public final class Connection : ConnectionType, Equatable { /// 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 @@ -518,18 +520,18 @@ public final class Connection : ConnectionType, Equatable { /// 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 @@ -537,7 +539,7 @@ public final class Connection : ConnectionType, Equatable { try self.run(commit) } } - + /// Interrupts any long-running queries. public func interrupt() { sqlite3_interrupt(handle) @@ -767,7 +769,7 @@ public final class Connection : ConnectionType, Equatable { // MARK: - Error Handling - func sync(block: () throws -> T) rethrows -> T { + public func sync(block: () throws -> T) rethrows -> T { var success: T? var failure: ErrorType? @@ -800,7 +802,7 @@ public final class Connection : ConnectionType, Equatable { } -extension Connection : CustomStringConvertible { +extension DBConnection : CustomStringConvertible { public var description: String { return String.fromCString(sqlite3_db_filename(handle, nil))! @@ -808,7 +810,7 @@ extension Connection : CustomStringConvertible { } -extension Connection.Location : CustomStringConvertible { +extension DBConnection.Location : CustomStringConvertible { public var description: String { switch self { @@ -823,7 +825,7 @@ extension Connection.Location : CustomStringConvertible { } -public func ==(lhs: Connection, rhs: Connection) -> Bool { +public func ==(lhs: DBConnection, rhs: DBConnection) -> Bool { return lhs === rhs } @@ -860,7 +862,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: DBConnection, 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 index d4a0c3f5..87c12845 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -39,18 +39,19 @@ public protocol ConnectionPoolDelegate { // WAL mode. public final class ConnectionPool { - private let location : Connection.Location - private var availableReadConnections = [Connection]() - private var unavailableReadConnections = [Connection]() + private let location : DBConnection.Location + private var availableReadConnections = [DBConnection]() + private var unavailableReadConnections = [DBConnection]() private let lockQueue : dispatch_queue_t - private var writeConnection : Connection! + private var writeConnection : DBConnection! + private let writeQueue : dispatch_queue_t public var delegate : ConnectionPoolDelegate? - public init(_ location: Connection.Location) throws { + public init(_ location: DBConnection.Location) throws { self.location = location - self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool", DISPATCH_QUEUE_SERIAL) - + self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool.Lock", DISPATCH_QUEUE_SERIAL) + self.writeQueue = dispatch_queue_create("SQLite.ConnectionPool.Write", DISPATCH_QUEUE_SERIAL) try writable.execute("PRAGMA locking_mode = EXCLUSIVE; PRAGMA journal_mode = WAL;") } @@ -64,12 +65,12 @@ public final class ConnectionPool { // Connection that automatically returns itself // to the pool when it goes out of scope - private class BorrowedConnection : ConnectionType, Equatable { + private class BorrowedConnection : Connection, Equatable { let pool : ConnectionPool - let connection : Connection + let connection : DBConnection - init(pool: ConnectionPool, connection: Connection) { + init(pool: ConnectionPool, connection: DBConnection) { self.pool = pool self.connection = connection } @@ -101,19 +102,23 @@ public final class ConnectionPool { @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: () throws -> Void) throws { return try connection.transaction(mode, block: block) } - func savepoint(name: String, block: () throws -> Void) throws { return try connection.savepoint(name, block: block) } + 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 - public var writable : ConnectionType { + public var writable : DBConnection { + var writeConnectionInit = dispatch_once_t() dispatch_once(&writeConnectionInit) { let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL - self.writeConnection = try! Connection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.WriteConnection")) + self.writeConnection = try! DBConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.WriteConnection")) self.writeConnection.busyTimeout = 2 } @@ -121,7 +126,7 @@ public final class ConnectionPool { } // Acquires a read only connection to the database - public var readable : ConnectionType { + public var readable : Connection { var borrowed : BorrowedConnection! @@ -129,7 +134,7 @@ public final class ConnectionPool { dispatch_sync(lockQueue) { - let connection : Connection + let connection : DBConnection if let availableConnection = self.availableReadConnections.popLast() { connection = availableConnection @@ -137,7 +142,7 @@ public final class ConnectionPool { else if self.delegate?.poolShouldAddConnection(self) ?? true { let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL - connection = try! Connection(self.location, flags: flags, dispatcher: ImmediateDispatcher()) + connection = try! DBConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher()) connection.busyTimeout = 2 self.delegate?.pool(self, didAddConnection: connection) diff --git a/SQLite/Core/Statement.swift b/SQLite/Core/Statement.swift index 39fb000d..07942ae0 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: DBConnection - init(_ connection: Connection, _ SQL: String) throws { + init(_ connection: DBConnection, _ 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..351bb721 100644 --- a/SQLite/Extensions/FTS4.swift +++ b/SQLite/Extensions/FTS4.swift @@ -140,7 +140,7 @@ extension Tokenizer : CustomStringConvertible { } -extension Connection { +extension DBConnection { 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..e32ab3c4 100644 --- a/SQLite/Typed/CustomFunctions.swift +++ b/SQLite/Typed/CustomFunctions.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public extension Connection { +public extension DBConnection { /// 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 index 12e6402f..349891ec 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -10,7 +10,7 @@ class ConnectionPoolTests : SQLiteTestCase { func testConcurrentAccess() { let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite")) + let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) let conn = pool.writable try! conn.execute("CREATE TABLE IF NOT EXISTS test(id INTEGER PRIMARY KEY, name TEXT)") @@ -53,7 +53,7 @@ class ConnectionPoolTests : SQLiteTestCase { } - for x in 10..<500 { + for x in 10..<50000 { let name = "test" + String(x) let idx = Int(rand()) % 5 @@ -75,7 +75,7 @@ class ConnectionPoolTests : SQLiteTestCase { func testAutoRelease() { let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLiteswiftTests.sqlite")) + let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) do { try! pool.readable.execute("SELECT 1") @@ -83,5 +83,5 @@ class ConnectionPoolTests : SQLiteTestCase { XCTAssertEqual(pool.totalReadableConnectionCount, pool.availableReadableConnectionCount) } - + } diff --git a/SQLiteTests/ConnectionTests.swift b/SQLiteTests/ConnectionTests.swift index aeec9b72..ab43c52d 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! DBConnection(.InMemory) XCTAssertEqual("", db.description) } func test_init_returnsInMemoryByDefault() { - let db = try! Connection() + let db = try! DBConnection() XCTAssertEqual("", db.description) } func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! Connection(.Temporary) + let db = try! DBConnection(.Temporary) XCTAssertEqual("", db.description) } func test_init_withURI_returnsURIConnection() { - let db = try! Connection(.URI("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + let db = try! DBConnection(.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! DBConnection("\(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! DBConnection(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..0854b410 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! DBConnection() let users = Table("users") From 66b82b0e0ac957518ce8afe115538c41a080f47f Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Fri, 15 Apr 2016 23:25:26 -0700 Subject: [PATCH 04/19] Use vfs for exclusivity vfs seems to be a more reliable handling of exclusive mode. --- SQLite/Core/Connection.swift | 13 +++++++++---- SQLite/Core/ConnectionPool.swift | 22 ++++++++++++++-------- SQLiteTests/ConnectionPoolTests.swift | 4 ++-- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift index e1b82b76..a2574af2 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -268,9 +268,9 @@ public final class DBConnection : Connection, Equatable { /// Default: `false`. /// /// - Returns: A new database connection. - public convenience 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 self.init(location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.Connection")) + try self.init(location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.Connection"), vfsName: vfsName) } /// Initializes a new SQLite connection. @@ -285,9 +285,14 @@ public final class DBConnection : Connection, Equatable { /// - dispatcher: Dispatcher synchronization blocks /// /// - Returns: A new database connection. - public init(_ location: Location, flags: Int32, dispatcher: Dispatcher) throws { + public init(_ location: Location, flags: Int32, dispatcher: Dispatcher, vfsName: String? = nil) throws { self.dispatcher = dispatcher - try check(sqlite3_open_v2(location.description, &_handle, flags, nil)) + 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)) } diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index 87c12845..c231f4ce 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -25,6 +25,9 @@ import Foundation +private let vfsName = "unix-excl" + + /// Connection pool delegate public protocol ConnectionPoolDelegate { @@ -44,15 +47,13 @@ public final class ConnectionPool { private var unavailableReadConnections = [DBConnection]() private let lockQueue : dispatch_queue_t private var writeConnection : DBConnection! - private let writeQueue : dispatch_queue_t public var delegate : ConnectionPoolDelegate? public init(_ location: DBConnection.Location) throws { self.location = location self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool.Lock", DISPATCH_QUEUE_SERIAL) - self.writeQueue = dispatch_queue_create("SQLite.ConnectionPool.Write", DISPATCH_QUEUE_SERIAL) - try writable.execute("PRAGMA locking_mode = EXCLUSIVE; PRAGMA journal_mode = WAL;") + try writable.execute("PRAGMA journal_mode = WAL;") } public var totalReadableConnectionCount : Int { @@ -114,12 +115,16 @@ public final class ConnectionPool { // Acquires a read/write connection to the database public var writable : DBConnection { - var writeConnectionInit = dispatch_once_t() dispatch_once(&writeConnectionInit) { - let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL - self.writeConnection = try! DBConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.WriteConnection")) + + let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX + self.writeConnection = try! DBConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.ConnectionPool.Write"), vfsName: vfsName) self.writeConnection.busyTimeout = 2 + + if let delegate = self.delegate { + delegate.pool(self, didAddConnection: self.writeConnection) + } } return writeConnection @@ -141,8 +146,9 @@ public final class ConnectionPool { } else if self.delegate?.poolShouldAddConnection(self) ?? true { - let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL - connection = try! DBConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher()) + let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX + + connection = try! DBConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher(), vfsName: vfsName) connection.busyTimeout = 2 self.delegate?.pool(self, didAddConnection: connection) diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index 349891ec..47ee1f31 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -34,7 +34,7 @@ class ConnectionPoolTests : SQLiteTestCase { let conn = pool.readable - let stmt = conn.prepare("SELECT name FROM test WHERE id = ?") + let stmt = try! conn.prepare("SELECT name FROM test WHERE id = ?") var curr = stmt.scalar(x) as! String while !quit { @@ -65,7 +65,7 @@ class ConnectionPoolTests : SQLiteTestCase { XCTFail((error as? CustomStringConvertible)?.description ?? "Unknown") } - usleep(15000) + usleep(1500) } quit = true From ccc20e574a6bab6be91f452f0ecf141b18011deb Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Sat, 16 Apr 2016 01:15:49 -0700 Subject: [PATCH 05/19] Added alternate concurrency test --- SQLiteTests/ConnectionPoolTests.swift | 52 ++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index 47ee1f31..ce460fe5 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -7,13 +7,13 @@ class ConnectionPoolTests : SQLiteTestCase { super.setUp() } - func testConcurrentAccess() { + func testConcurrentAccess2() { let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) let conn = pool.writable - try! conn.execute("CREATE TABLE IF NOT EXISTS test(id INTEGER PRIMARY KEY, name TEXT)") + 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')") @@ -40,7 +40,7 @@ class ConnectionPoolTests : SQLiteTestCase { let now = stmt.scalar(x) as! String if now != curr { - print(now) + //print(now) curr = now } reads += 1 @@ -53,7 +53,7 @@ class ConnectionPoolTests : SQLiteTestCase { } - for x in 10..<50000 { + for x in 10..<5000 { let name = "test" + String(x) let idx = Int(rand()) % 5 @@ -65,13 +65,55 @@ class ConnectionPoolTests : SQLiteTestCase { XCTFail((error as? CustomStringConvertible)?.description ?? "Unknown") } - usleep(1500) + usleep(500) } quit = true waitForExpectationsWithTimeout(1000, handler: nil) } + func testConcurrentAccess() throws { + + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") + let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) + + 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 = 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() { let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") From 18f612fd112bdedc6fecec558fb27cd119a2c59e Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Sat, 16 Apr 2016 01:23:10 -0700 Subject: [PATCH 06/19] Fix imports in ConnectionPool.swift --- SQLite/Core/ConnectionPool.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index c231f4ce..5166e6eb 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -22,7 +22,8 @@ // THE SOFTWARE. // -import Foundation +import Dispatch +import CSQLite private let vfsName = "unix-excl" From 2a5908b513d5b44b1070f2667011f1972482bbd4 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Sun, 17 Apr 2016 15:55:59 -0700 Subject: [PATCH 07/19] Rename `DBConnection` to `DirectConnection` --- SQLite.xcodeproj/project.pbxproj | 12 ++++++------ SQLite/Core/Connection.swift | 10 +++++----- SQLite/Core/ConnectionPool.swift | 22 +++++++++++----------- SQLite/Core/Statement.swift | 4 ++-- SQLite/Extensions/FTS4.swift | 2 +- SQLite/Typed/CustomFunctions.swift | 2 +- SQLiteTests/ConnectionTests.swift | 12 ++++++------ SQLiteTests/TestHelpers.swift | 2 +- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj index edc79e3f..6b145ca1 100644 --- a/SQLite.xcodeproj/project.pbxproj +++ b/SQLite.xcodeproj/project.pbxproj @@ -173,7 +173,7 @@ 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; path = ConnectionPool.swift; sourceTree = ""; }; + 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; }; @@ -182,19 +182,19 @@ 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 = ""; }; @@ -204,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 = ""; }; diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift index a2574af2..4ae468dd 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -227,7 +227,7 @@ public protocol Connection { /// A connection to SQLite. -public final class DBConnection : Connection, Equatable { +public final class DirectConnection : Connection, Equatable { /// The location of a SQLite database. public enum Location { @@ -807,7 +807,7 @@ public final class DBConnection : Connection, Equatable { } -extension DBConnection : CustomStringConvertible { +extension DirectConnection : CustomStringConvertible { public var description: String { return String.fromCString(sqlite3_db_filename(handle, nil))! @@ -815,7 +815,7 @@ extension DBConnection : CustomStringConvertible { } -extension DBConnection.Location : CustomStringConvertible { +extension DirectConnection.Location : CustomStringConvertible { public var description: String { switch self { @@ -830,7 +830,7 @@ extension DBConnection.Location : CustomStringConvertible { } -public func ==(lhs: DBConnection, rhs: DBConnection) -> Bool { +public func ==(lhs: DirectConnection, rhs: DirectConnection) -> Bool { return lhs === rhs } @@ -867,7 +867,7 @@ public enum Result : ErrorType { case Error(message: String, code: Int32, statement: Statement?) - init?(errorCode: Int32, connection: DBConnection, 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 index 5166e6eb..931e5874 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -43,15 +43,15 @@ public protocol ConnectionPoolDelegate { // WAL mode. public final class ConnectionPool { - private let location : DBConnection.Location - private var availableReadConnections = [DBConnection]() - private var unavailableReadConnections = [DBConnection]() + private let location : DirectConnection.Location + private var availableReadConnections = [DirectConnection]() + private var unavailableReadConnections = [DirectConnection]() private let lockQueue : dispatch_queue_t - private var writeConnection : DBConnection! + private var writeConnection : DirectConnection! public var delegate : ConnectionPoolDelegate? - public init(_ location: DBConnection.Location) throws { + public init(_ location: DirectConnection.Location) throws { self.location = location self.lockQueue = dispatch_queue_create("SQLite.ConnectionPool.Lock", DISPATCH_QUEUE_SERIAL) try writable.execute("PRAGMA journal_mode = WAL;") @@ -70,9 +70,9 @@ public final class ConnectionPool { private class BorrowedConnection : Connection, Equatable { let pool : ConnectionPool - let connection : DBConnection + let connection : DirectConnection - init(pool: ConnectionPool, connection: DBConnection) { + init(pool: ConnectionPool, connection: DirectConnection) { self.pool = pool self.connection = connection } @@ -114,13 +114,13 @@ public final class ConnectionPool { // Acquires a read/write connection to the database - public var writable : DBConnection { + public var writable : DirectConnection { var writeConnectionInit = dispatch_once_t() dispatch_once(&writeConnectionInit) { let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX - self.writeConnection = try! DBConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.ConnectionPool.Write"), vfsName: vfsName) + self.writeConnection = try! DirectConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.ConnectionPool.Write"), vfsName: vfsName) self.writeConnection.busyTimeout = 2 if let delegate = self.delegate { @@ -140,7 +140,7 @@ public final class ConnectionPool { dispatch_sync(lockQueue) { - let connection : DBConnection + let connection : DirectConnection if let availableConnection = self.availableReadConnections.popLast() { connection = availableConnection @@ -149,7 +149,7 @@ public final class ConnectionPool { let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX - connection = try! DBConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher(), vfsName: vfsName) + connection = try! DirectConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher(), vfsName: vfsName) connection.busyTimeout = 2 self.delegate?.pool(self, didAddConnection: connection) diff --git a/SQLite/Core/Statement.swift b/SQLite/Core/Statement.swift index 07942ae0..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: DBConnection + private let connection: DirectConnection - init(_ connection: DBConnection, _ 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 351bb721..045e5303 100644 --- a/SQLite/Extensions/FTS4.swift +++ b/SQLite/Extensions/FTS4.swift @@ -140,7 +140,7 @@ extension Tokenizer : CustomStringConvertible { } -extension DBConnection { +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 e32ab3c4..d004f75e 100644 --- a/SQLite/Typed/CustomFunctions.swift +++ b/SQLite/Typed/CustomFunctions.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public extension DBConnection { +public extension DirectConnection { /// Creates or redefines a custom SQL function. /// diff --git a/SQLiteTests/ConnectionTests.swift b/SQLiteTests/ConnectionTests.swift index ab43c52d..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! DBConnection(.InMemory) + let db = try! DirectConnection(.InMemory) XCTAssertEqual("", db.description) } func test_init_returnsInMemoryByDefault() { - let db = try! DBConnection() + let db = try! DirectConnection() XCTAssertEqual("", db.description) } func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! DBConnection(.Temporary) + let db = try! DirectConnection(.Temporary) XCTAssertEqual("", db.description) } func test_init_withURI_returnsURIConnection() { - let db = try! DBConnection(.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! DBConnection("\(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! DBConnection(readonly: true) + let db = try! DirectConnection(readonly: true) XCTAssertTrue(db.readonly) } diff --git a/SQLiteTests/TestHelpers.swift b/SQLiteTests/TestHelpers.swift index 0854b410..855bf7bd 100644 --- a/SQLiteTests/TestHelpers.swift +++ b/SQLiteTests/TestHelpers.swift @@ -5,7 +5,7 @@ class SQLiteTestCase : XCTestCase { var trace = [String: Int]() - let db = try! DBConnection() + let db = try! DirectConnection() let users = Table("users") From 9548d172655ed841be1ba31600cf47417c58389c Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 18 Apr 2016 14:41:19 -0700 Subject: [PATCH 08/19] Replace pool delegate with setup closures --- SQLite/Core/ConnectionPool.swift | 58 ++++++++++++++++++--------- SQLiteTests/ConnectionPoolTests.swift | 13 ++++++ 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index 931e5874..d430d7cf 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -29,15 +29,6 @@ import CSQLite private let vfsName = "unix-excl" -/// Connection pool delegate -public protocol ConnectionPoolDelegate { - - func poolShouldAddConnection(pool: ConnectionPool) -> Bool - func pool(pool: ConnectionPool, didAddConnection: Connection) - -} - - // Connection pool for accessing an SQLite database // with multiple readers & a single writer. Utilizes // WAL mode. @@ -49,12 +40,29 @@ public final class ConnectionPool { private let lockQueue : dispatch_queue_t private var writeConnection : DirectConnection! - public var delegate : ConnectionPoolDelegate? + 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) - try writable.execute("PRAGMA journal_mode = WAL;") + self.internalSetup[.WriteAheadLogging] = { try $0.execute("PRAGMA journal_mode = WAL;") } } public var totalReadableConnectionCount : Int { @@ -123,9 +131,14 @@ public final class ConnectionPool { self.writeConnection = try! DirectConnection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.ConnectionPool.Write"), vfsName: vfsName) self.writeConnection.busyTimeout = 2 - if let delegate = self.delegate { - delegate.pool(self, didAddConnection: self.writeConnection) + for setupProcessor in self.internalSetup.values { + try! setupProcessor(self.writeConnection) } + + for setupProcessor in self.setup { + try! setupProcessor(self.writeConnection) + } + } return writeConnection @@ -140,23 +153,32 @@ public final class ConnectionPool { dispatch_sync(lockQueue) { + // Ensure database is open + self.writable + let connection : DirectConnection if let availableConnection = self.availableReadConnections.popLast() { connection = availableConnection } - else if self.delegate?.poolShouldAddConnection(self) ?? true { + 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 - self.delegate?.pool(self, didAddConnection: connection) + for (type, setupProcessor) in self.internalSetup { + if type == .WriteAheadLogging { + continue + } + try! setupProcessor(connection) + } + + for setupProcessor in self.setup { + try! setupProcessor(connection) + } - } - else { - return } self.unavailableReadConnections.append(connection) diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index ce460fe5..b621396c 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -6,6 +6,19 @@ class ConnectionPoolTests : SQLiteTestCase { override func setUp() { super.setUp() } + + func testConnectionSetupClosures() { + + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") + let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) + + pool.foreignKeys = true + pool.setup.append { try $0.execute("CREATE TABLE IF NOT EXISTS test(value INT)") } + + XCTAssertTrue(try 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() { From ffb92745ad76b7a5eb2e3e293da755fa8d760a7a Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 18 Apr 2016 17:18:22 -0700 Subject: [PATCH 09/19] Fix writable connection initialization Was using incorrect dispatch_once token. --- SQLite/Core/ConnectionPool.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index d430d7cf..dd644304 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -122,9 +122,11 @@ public final class ConnectionPool { // Acquires a read/write connection to the database + + var writeConnectionInit = dispatch_once_t() + public var writable : DirectConnection { - - var writeConnectionInit = dispatch_once_t() + dispatch_once(&writeConnectionInit) { let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX From b7a0eb1a58936071a6d171036f3e36f75af8f401 Mon Sep 17 00:00:00 2001 From: Kevin Wooten Date: Mon, 18 Apr 2016 17:51:38 -0700 Subject: [PATCH 10/19] Cleanup connection pool tests # Conflicts: # SQLiteTests/ConnectionPoolTests.swift --- SQLiteTests/ConnectionPoolTests.swift | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index b621396c..74351527 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -3,28 +3,25 @@ import SQLite class ConnectionPoolTests : SQLiteTestCase { + var pool : ConnectionPool! + override func setUp() { - super.setUp() + let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") + pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) } func testConnectionSetupClosures() { - let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) - pool.foreignKeys = true pool.setup.append { try $0.execute("CREATE TABLE IF NOT EXISTS test(value INT)") } - XCTAssertTrue(try pool.readable.scalar("PRAGMA foreign_keys") as! Int64 == 1) + 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 _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) - 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") @@ -45,7 +42,7 @@ class ConnectionPoolTests : SQLiteTestCase { print("started", x) - let conn = pool.readable + let conn = self.pool.readable let stmt = try! conn.prepare("SELECT name FROM test WHERE id = ?") var curr = stmt.scalar(x) as! String @@ -87,9 +84,6 @@ class ConnectionPoolTests : SQLiteTestCase { func testConcurrentAccess() throws { - let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) - try! pool.writable.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") try! pool.writable.run("INSERT INTO test(value) VALUES(?)", 0) @@ -102,7 +96,7 @@ class ConnectionPoolTests : SQLiteTestCase { while !finished { - let val = pool.readable.scalar("SELECT value FROM test") + let val = self.pool.readable.scalar("SELECT value FROM test") assert(val != nil, "DB query returned nil result set") } @@ -129,9 +123,6 @@ class ConnectionPoolTests : SQLiteTestCase { func testAutoRelease() { - let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - let pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) - do { try! pool.readable.execute("SELECT 1") } From 960309eb02724ae0f18f721942fb22473bb4b602 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Thu, 16 Jun 2016 15:48:36 -0600 Subject: [PATCH 11/19] Implement connection limit. --- SQLite/Core/ConnectionPool.swift | 55 +++++++++++++-------------- SQLiteTests/ConnectionPoolTests.swift | 21 +++++----- 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index dd644304..77cf047e 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -39,6 +39,7 @@ public final class ConnectionPool { private var unavailableReadConnections = [DirectConnection]() private let lockQueue : dispatch_queue_t private var writeConnection : DirectConnection! + private let connectionSemaphore = dispatch_semaphore_create(5) public var foreignKeys : Bool { get { @@ -91,6 +92,7 @@ public final class ConnectionPool { self.pool.unavailableReadConnections.removeAtIndex(index) } self.pool.availableReadConnections.append(self.connection) + dispatch_semaphore_signal(self.pool.connectionSemaphore) } } @@ -151,44 +153,41 @@ public final class ConnectionPool { var borrowed : BorrowedConnection! - repeat { + dispatch_semaphore_wait(connectionSemaphore, DISPATCH_TIME_FOREVER) + dispatch_sync(lockQueue) { - dispatch_sync(lockQueue) { - - // Ensure database is open - self.writable + // Ensure database is open + self.writable + + let connection : DirectConnection + + if let availableConnection = self.availableReadConnections.popLast() { + connection = availableConnection + } + else { - let connection : DirectConnection + let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX - 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 + 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) + for (type, setupProcessor) in self.internalSetup { + if type == .WriteAheadLogging { + continue } - + try! setupProcessor(connection) } - self.unavailableReadConnections.append(connection) + for setupProcessor in self.setup { + try! setupProcessor(connection) + } - borrowed = BorrowedConnection(pool: self, connection: connection) } - } while borrowed == nil + self.unavailableReadConnections.append(connection) + + borrowed = BorrowedConnection(pool: self, connection: connection) + } return borrowed } diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index 74351527..b7f15171 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -22,33 +22,32 @@ class ConnectionPoolTests : SQLiteTestCase { func testConcurrentAccess2() { + let threadCount = 20 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')") + for threadNumber in 0.. Date: Wed, 22 Jun 2016 12:37:10 -0600 Subject: [PATCH 12/19] Fix import problem for standalone. --- SQLite/Core/ConnectionPool.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index 77cf047e..3f4a01f4 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -23,7 +23,11 @@ // import Dispatch -import CSQLite +#if SQLITE_SWIFT_STANDALONE + import sqlite3 +#else + import CSQLite +#endif private let vfsName = "unix-excl" From e32db0905f244a46a4c61957e6c69bf856c7b155 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Wed, 22 Jun 2016 16:04:46 -0600 Subject: [PATCH 13/19] Shorten testConcurrentAccess2 time as it is making the travis tests take too long. --- SQLiteTests/ConnectionPoolTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index b7f15171..301f0482 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -62,10 +62,10 @@ class ConnectionPoolTests : SQLiteTestCase { } - for x in 10..<5000 { + for x in 10..<1000 { let name = "test" + String(x) - let idx = Int(rand()) % 5 + let idx = Int(rand()) % threadCount do { try conn.run("UPDATE test SET name=? WHERE id=?", name, idx) From 2fd69b87211c10428e75dbc6cdbce647ff866194 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Thu, 23 Jun 2016 08:59:13 -0600 Subject: [PATCH 14/19] Tests are still taking too long. --- SQLite/Core/ConnectionPool.swift | 4 ++-- SQLiteTests/ConnectionPoolTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index 3f4a01f4..1e6a4b44 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -24,9 +24,9 @@ import Dispatch #if SQLITE_SWIFT_STANDALONE - import sqlite3 +import sqlite3 #else - import CSQLite +import CSQLite #endif diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index 301f0482..190b70a0 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -62,7 +62,7 @@ class ConnectionPoolTests : SQLiteTestCase { } - for x in 10..<1000 { + for x in 10..<100 { let name = "test" + String(x) let idx = Int(rand()) % threadCount From a688d1841f3d88e49a122221c9de309b467965b7 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Mon, 18 Jul 2016 14:26:48 -0600 Subject: [PATCH 15/19] Clean up some spacing. --- SQLite/Core/Connection.swift | 8 ++-- SQLite/Core/ConnectionPool.swift | 26 +++++----- SQLite/Core/Dispatcher.swift | 82 +++++++++++++++++++------------- SQLite/Typed/Query.swift | 1 - 4 files changed, 66 insertions(+), 51 deletions(-) diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift index 5071c7ae..f66fb064 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -50,14 +50,14 @@ public enum TransactionMode : String { public protocol Connection { /// Whether or not the database was opened in a read-only state. - var readonly : Bool { get } + var readonly: Bool { get } /// The last rowid inserted into the database via this connection. - var lastInsertRowid : Int64? { get } + var lastInsertRowid: Int64? { get } /// The last number of changes (inserts, updates, or deletes) made to the /// database via this connection. - var changes : Int { get } + var changes: Int { get } /// The total number of changes (inserts, updates, or deletes) made to the /// database via this connection. @@ -808,7 +808,7 @@ public final class DirectConnection : Connection, Equatable { throw error } - private var dispatcher : Dispatcher + private var dispatcher: Dispatcher } diff --git a/SQLite/Core/ConnectionPool.swift b/SQLite/Core/ConnectionPool.swift index 1e6a4b44..035c706a 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -38,11 +38,11 @@ private let vfsName = "unix-excl" // WAL mode. public final class ConnectionPool { - private let location : DirectConnection.Location + private let location: DirectConnection.Location private var availableReadConnections = [DirectConnection]() private var unavailableReadConnections = [DirectConnection]() - private let lockQueue : dispatch_queue_t - private var writeConnection : DirectConnection! + private let lockQueue: dispatch_queue_t + private var writeConnection: DirectConnection! private let connectionSemaphore = dispatch_semaphore_create(5) public var foreignKeys : Bool { @@ -82,8 +82,8 @@ public final class ConnectionPool { // to the pool when it goes out of scope private class BorrowedConnection : Connection, Equatable { - let pool : ConnectionPool - let connection : DirectConnection + let pool: ConnectionPool + let connection: DirectConnection init(pool: ConnectionPool, connection: DirectConnection) { self.pool = pool @@ -100,10 +100,10 @@ public final class ConnectionPool { } } - var readonly : Bool { return connection.readonly } - var lastInsertRowid : Int64? { return connection.lastInsertRowid } - var changes : Int { return connection.changes } - var totalChanges : Int { return connection.totalChanges } + 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) } @@ -131,7 +131,7 @@ public final class ConnectionPool { var writeConnectionInit = dispatch_once_t() - public var writable : DirectConnection { + public var writable: DirectConnection { dispatch_once(&writeConnectionInit) { @@ -153,9 +153,9 @@ public final class ConnectionPool { } // Acquires a read only connection to the database - public var readable : Connection { + public var readable: Connection { - var borrowed : BorrowedConnection! + var borrowed: BorrowedConnection! dispatch_semaphore_wait(connectionSemaphore, DISPATCH_TIME_FOREVER) dispatch_sync(lockQueue) { @@ -163,7 +163,7 @@ public final class ConnectionPool { // Ensure database is open self.writable - let connection : DirectConnection + let connection: DirectConnection if let availableConnection = self.availableReadConnections.popLast() { connection = availableConnection diff --git a/SQLite/Core/Dispatcher.swift b/SQLite/Core/Dispatcher.swift index b5010488..f6cff005 100644 --- a/SQLite/Core/Dispatcher.swift +++ b/SQLite/Core/Dispatcher.swift @@ -1,9 +1,25 @@ // -// Dispatcher.swift -// SQLite +// SQLite.swift +// https://github.com/stephencelis/SQLite.swift +// Copyright © 2014-2015 Stephen Celis. // -// Created by Kevin Wooten on 11/28/15. -// Copyright © 2015 stephencelis. All rights reserved. +// 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 Foundation @@ -11,45 +27,45 @@ import Foundation /// Block dispatch method public protocol Dispatcher { - - /// Dispatches the provided block - func dispatch(block: dispatch_block_t) - + + /// 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() - } - + + 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 + + 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/Typed/Query.swift b/SQLite/Typed/Query.swift index 9f966250..30a57337 100644 --- a/SQLite/Typed/Query.swift +++ b/SQLite/Typed/Query.swift @@ -1051,7 +1051,6 @@ public struct Row { } // FIXME: rdar://problem/18673897 // subscript… - public subscript(column: Expression) -> Blob { return get(column) } From a1121c42101483fd469f393eb63420e083e7cd60 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Mon, 18 Jul 2016 16:06:18 -0600 Subject: [PATCH 16/19] Change ConnectionPool API and get rid of Connection protocol. --- SQLite/Core/Connection.swift | 215 ++------------------------ SQLite/Core/ConnectionPool.swift | 114 +++++++------- SQLite/Core/Statement.swift | 4 +- SQLite/Extensions/FTS4.swift | 2 +- SQLite/Typed/CustomFunctions.swift | 2 +- SQLiteTests/ConnectionPoolTests.swift | 131 +++++++++++----- SQLiteTests/ConnectionTests.swift | 12 +- SQLiteTests/TestHelpers.swift | 2 +- 8 files changed, 168 insertions(+), 314 deletions(-) diff --git a/SQLite/Core/Connection.swift b/SQLite/Core/Connection.swift index f66fb064..d594a4f5 100644 --- a/SQLite/Core/Connection.swift +++ b/SQLite/Core/Connection.swift @@ -46,193 +46,8 @@ public enum TransactionMode : String { } -/// 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 DirectConnection : Connection, Equatable { +public final class Connection : Equatable { /// The location of a SQLite database. public enum Location { @@ -273,24 +88,12 @@ public final class DirectConnection : Connection, Equatable { /// Default: `false`. /// /// - Returns: A new database connection. - public convenience init(_ location: Location = .InMemory, readonly: Bool = false, vfsName: String? = nil) throws { + public convenience init(_ location: Location = .InMemory, readonly: Bool = false) throws { let flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE - try self.init(location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.Connection"), vfsName: vfsName) + try self.init(location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.Connection"), vfsName: nil) } - - /// 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 { + + 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)) @@ -812,7 +615,7 @@ public final class DirectConnection : Connection, Equatable { } -extension DirectConnection : CustomStringConvertible { +extension Connection : CustomStringConvertible { public var description: String { return String.fromCString(sqlite3_db_filename(handle, nil))! @@ -820,7 +623,7 @@ extension DirectConnection : CustomStringConvertible { } -extension DirectConnection.Location : CustomStringConvertible { +extension Connection.Location : CustomStringConvertible { public var description: String { switch self { @@ -835,7 +638,7 @@ extension DirectConnection.Location : CustomStringConvertible { } -public func ==(lhs: DirectConnection, rhs: DirectConnection) -> Bool { +public func == (lhs: Connection, rhs: Connection) -> Bool { return lhs === rhs } @@ -872,7 +675,7 @@ public enum Result : ErrorType { case Error(message: String, code: Int32, statement: Statement?) - init?(errorCode: Int32, connection: DirectConnection, statement: Statement? = nil) { + init?(errorCode: Int32, connection: Connection, 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 index 035c706a..91c3e366 100644 --- a/SQLite/Core/ConnectionPool.swift +++ b/SQLite/Core/ConnectionPool.swift @@ -38,11 +38,11 @@ private let vfsName = "unix-excl" // WAL mode. public final class ConnectionPool { - private let location: DirectConnection.Location - private var availableReadConnections = [DirectConnection]() - private var unavailableReadConnections = [DirectConnection]() + private let location: Connection.Location + private var availableReadConnections = [Connection]() + private var unavailableReadConnections = [Connection]() private let lockQueue: dispatch_queue_t - private var writeConnection: DirectConnection! + private var writeConnection: Connection! private let connectionSemaphore = dispatch_semaphore_create(5) public var foreignKeys : Bool { @@ -64,12 +64,38 @@ public final class ConnectionPool { private var internalSetup = [InternalOption: ConnectionProcessor]() - public init(_ location: DirectConnection.Location) throws { + /// Initializes a new SQLite connection pool. + /// + /// - Parameters: + /// + /// - location: The location of the database. Creates a new database if it + /// doesn’t already exist. + /// + /// Default: `.InMemory`. + /// + /// - Throws: `Result.Error` iff a connection cannot be established. + /// + /// - Returns: A new connection pool. + public init(_ location: Connection.Location = .InMemory) 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;") } } + /// Initializes a new connection to a database. + /// + /// - Parameters: + /// + /// - filename: The location of the database. Creates a new database if + /// it doesn’t already exist (unless in read-only mode). + /// + /// - Throws: `Result.Error` iff a connection cannot be established. + /// + /// - Returns: A new database connection pool. + public convenience init(_ filename: String) throws { + try self.init(.URI(filename)) + } + public var totalReadableConnectionCount : Int { return availableReadConnections.count + unavailableReadConnections.count } @@ -78,65 +104,36 @@ public final class ConnectionPool { 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 - } + /// Calls `readBlock` with an available read connection from the connection pool, + /// after which the connection is made available again. + public func read(readBlock: (connection: Connection) -> Void) { + let connection = readable + readBlock(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) - dispatch_semaphore_signal(self.pool.connectionSemaphore) + dispatch_sync(lockQueue) { + if let index = self.unavailableReadConnections.indexOf(connection) { + self.unavailableReadConnections.removeAtIndex(index) } + self.availableReadConnections.append(connection) + dispatch_semaphore_signal(self.connectionSemaphore) } - - 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) } - } + /// Calls `readWriteBlock` with a writeable connection + public func readWrite(readWriteBlock: (connection: Connection) -> Void) { + let connection = writable + readWriteBlock(connection: connection) + } // Acquires a read/write connection to the database - var writeConnectionInit = dispatch_once_t() - public var writable: DirectConnection { + private var writable: Connection { 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 = try! Connection(self.location, flags: flags, dispatcher: ReentrantDispatcher("SQLite.ConnectionPool.Write"), vfsName: vfsName) self.writeConnection.busyTimeout = 2 for setupProcessor in self.internalSetup.values { @@ -153,9 +150,9 @@ public final class ConnectionPool { } // Acquires a read only connection to the database - public var readable: Connection { + private var readable: Connection { - var borrowed: BorrowedConnection! + var borrowed: Connection! dispatch_semaphore_wait(connectionSemaphore, DISPATCH_TIME_FOREVER) dispatch_sync(lockQueue) { @@ -163,7 +160,7 @@ public final class ConnectionPool { // Ensure database is open self.writable - let connection: DirectConnection + let connection: Connection if let availableConnection = self.availableReadConnections.popLast() { connection = availableConnection @@ -172,7 +169,7 @@ public final class ConnectionPool { let flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_WAL | SQLITE_OPEN_NOMUTEX - connection = try! DirectConnection(self.location, flags: flags, dispatcher: ImmediateDispatcher(), vfsName: vfsName) + connection = try! Connection(self.location, flags: flags, dispatcher: ImmediateDispatcher(), vfsName: vfsName) connection.busyTimeout = 2 for (type, setupProcessor) in self.internalSetup { @@ -190,15 +187,10 @@ public final class ConnectionPool { self.unavailableReadConnections.append(connection) - borrowed = BorrowedConnection(pool: self, connection: connection) + borrowed = connection } return borrowed } } - - -private func ==(lhs: ConnectionPool.BorrowedConnection, rhs: ConnectionPool.BorrowedConnection) -> Bool { - return lhs.connection == rhs.connection -} diff --git a/SQLite/Core/Statement.swift b/SQLite/Core/Statement.swift index 876eab0f..9a4bfa1e 100644 --- a/SQLite/Core/Statement.swift +++ b/SQLite/Core/Statement.swift @@ -33,9 +33,9 @@ public final class Statement { private var handle: COpaquePointer = nil - private let connection: DirectConnection + private let connection: Connection - init(_ connection: DirectConnection, _ SQL: String) throws { + init(_ connection: Connection, _ 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 7224cde9..466c42c7 100644 --- a/SQLite/Extensions/FTS4.swift +++ b/SQLite/Extensions/FTS4.swift @@ -138,7 +138,7 @@ extension Tokenizer : CustomStringConvertible { } -extension DirectConnection { +extension Connection { 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 d004f75e..068d0340 100644 --- a/SQLite/Typed/CustomFunctions.swift +++ b/SQLite/Typed/CustomFunctions.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. // -public extension DirectConnection { +public extension Connection { /// Creates or redefines a custom SQL function. /// diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index 190b70a0..14632713 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -7,54 +7,55 @@ class ConnectionPoolTests : SQLiteTestCase { override func setUp() { let _ = try? NSFileManager.defaultManager().removeItemAtPath("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") - pool = try! ConnectionPool(.URI("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite")) + pool = try! ConnectionPool("\(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)") } + pool.read { conn in + XCTAssertTrue(conn.scalar("PRAGMA foreign_keys") as! Int64 == 1) + } + + pool.readWrite { conn in + try! conn.execute("INSERT INTO test(value) VALUES (1)") + try! conn.execute("SELECT value FROM test") + } - 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 threadCount = 20 - 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") - for threadNumber in 0.. 0, "Thread \(threadNumber) did not read.") print("ended at", reads, "reads") ex.fulfill() @@ -67,11 +68,13 @@ class ConnectionPoolTests : SQLiteTestCase { let name = "test" + String(x) let idx = Int(rand()) % threadCount - do { - try conn.run("UPDATE test SET name=? WHERE id=?", name, idx) - } - catch let error { - XCTFail((error as? CustomStringConvertible)?.description ?? "Unknown") + pool.readWrite { conn in + do { + try conn.run("UPDATE test SET name=? WHERE id=?", name, idx) + } + catch let error { + XCTFail((error as? CustomStringConvertible)?.description ?? "Unknown") + } } usleep(500) @@ -82,9 +85,10 @@ class ConnectionPoolTests : SQLiteTestCase { } 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) + pool.readWrite { conn in + try! conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") + try! conn.run("INSERT INTO test(value) VALUES(?)", 0) + } let q = dispatch_queue_create("Readers/Writers", DISPATCH_QUEUE_CONCURRENT); var finished = false @@ -94,10 +98,11 @@ class ConnectionPoolTests : SQLiteTestCase { dispatch_async(q) { while !finished { - - let val = self.pool.readable.scalar("SELECT value FROM test") + var val: Binding? + self.pool.read { conn in + val = conn.scalar("SELECT value FROM test") + } assert(val != nil, "DB query returned nil result set") - } } @@ -105,8 +110,9 @@ class ConnectionPoolTests : SQLiteTestCase { } for c in 0..<5000 { - - try pool.writable.run("INSERT INTO test(value) VALUES(?)", c) + pool.readWrite { conn in + try! conn.run("INSERT INTO test(value) VALUES(?)", c) + } usleep(100); @@ -120,10 +126,63 @@ class ConnectionPoolTests : SQLiteTestCase { } - func testAutoRelease() { + func testMultiplePools() { + let pool2 = try! ConnectionPool("\(NSTemporaryDirectory())/SQLite.swift Pool Tests.sqlite") + pool.readWrite { conn in + try! conn.execute("DROP TABLE IF EXISTS test; CREATE TABLE test(value);") + try! conn.run("INSERT INTO test(value) VALUES(?)", 0) + } + + let q = dispatch_queue_create("Readers/Writers", DISPATCH_QUEUE_CONCURRENT); + var finished = false - do { - try! pool.readable.execute("SELECT 1") + for _ in 0..<20 { + dispatch_async(q) { + while !finished { + var val: Binding? + self.pool.read { conn in + val = conn.scalar("SELECT value FROM test") + } + assert(val != nil, "DB query returned nil result set") + } + } + } + + for _ in 0..<20 { + dispatch_async(q) { + while !finished { + var val: Binding? + pool2.read { conn in + val = conn.scalar("SELECT value FROM test") + } + assert(val != nil, "DB query returned nil result set") + } + } + } + + for c in 0..<5000 { + pool.readWrite { conn in + try! conn.run("INSERT INTO test(value) VALUES(?)", c) + } + + pool2.readWrite { conn in + try! conn.run("INSERT INTO test(value) VALUES(?)", c + 5000) + } + + usleep(100); + } + + finished = true + + // Wait for readers to finish + dispatch_barrier_sync(q) { + } + + } + + func testAutoRelease() { + pool.read { conn in + try! conn.execute("SELECT 1") } XCTAssertEqual(pool.totalReadableConnectionCount, pool.availableReadableConnectionCount) diff --git a/SQLiteTests/ConnectionTests.swift b/SQLiteTests/ConnectionTests.swift index bbad19a8..62203362 100644 --- a/SQLiteTests/ConnectionTests.swift +++ b/SQLiteTests/ConnectionTests.swift @@ -10,27 +10,27 @@ class ConnectionTests : SQLiteTestCase { } func test_init_withInMemory_returnsInMemoryConnection() { - let db = try! DirectConnection(.InMemory) + let db = try! Connection(.InMemory) XCTAssertEqual("", db.description) } func test_init_returnsInMemoryByDefault() { - let db = try! DirectConnection() + let db = try! Connection() XCTAssertEqual("", db.description) } func test_init_withTemporary_returnsTemporaryConnection() { - let db = try! DirectConnection(.Temporary) + let db = try! Connection(.Temporary) XCTAssertEqual("", db.description) } func test_init_withURI_returnsURIConnection() { - let db = try! DirectConnection(.URI("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) + let db = try! Connection(.URI("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3")) XCTAssertEqual("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3", db.description) } func test_init_withString_returnsURIConnection() { - let db = try! DirectConnection("\(NSTemporaryDirectory())/SQLite.swift Tests.sqlite3") + let db = try! Connection("\(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! DirectConnection(readonly: true) + let db = try! Connection(readonly: true) XCTAssertTrue(db.readonly) } diff --git a/SQLiteTests/TestHelpers.swift b/SQLiteTests/TestHelpers.swift index 855bf7bd..464b9c27 100644 --- a/SQLiteTests/TestHelpers.swift +++ b/SQLiteTests/TestHelpers.swift @@ -5,7 +5,7 @@ class SQLiteTestCase : XCTestCase { var trace = [String: Int]() - let db = try! DirectConnection() + let db = try! Connection() let users = Table("users") From 476cdc550430cbda86626d50af1a47c8d678f138 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Mon, 18 Jul 2016 16:52:13 -0600 Subject: [PATCH 17/19] Make sure every thread has at least one read. --- SQLiteTests/ConnectionPoolTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SQLiteTests/ConnectionPoolTests.swift b/SQLiteTests/ConnectionPoolTests.swift index 14632713..5a20485b 100644 --- a/SQLiteTests/ConnectionPoolTests.swift +++ b/SQLiteTests/ConnectionPoolTests.swift @@ -48,14 +48,13 @@ class ConnectionPoolTests : SQLiteTestCase { print("started", threadNumber) - while !quit { + while !quit || reads <= 0 { self.pool.read { conn in let _ = try! conn.prepare("SELECT name FROM test WHERE id = ?").scalar(threadNumber) as! String reads += 1 } } - XCTAssertTrue(reads > 0, "Thread \(threadNumber) did not read.") print("ended at", reads, "reads") ex.fulfill() From c7db9198cdd3afbb4801d0652588b8b5e7752f06 Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Tue, 19 Jul 2016 12:45:51 -0600 Subject: [PATCH 18/19] Add connection pool documentation. --- Documentation/Index.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Documentation/Index.md b/Documentation/Index.md index 0579e6f3..6ea09026 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -11,6 +11,7 @@ - [Read-Only Databases](#read-only-databases) - [In-Memory Databases](#in-memory-databases) - [Thread-Safety](#thread-safety) + - [Connection Pools](#connection-pools) - [Building Type-Safe SQL](#building-type-safe-sql) - [Expressions](#expressions) - [Compound Expressions](#compound-expressions) @@ -251,7 +252,7 @@ Every Connection comes equipped with its own serial queue for statement executio If you maintain multiple connections for a single database, consider setting a timeout (in seconds) and/or a busy handler: -```swift +``` swift db.busyTimeout = 5 db.busyHandler({ tries in @@ -265,6 +266,33 @@ db.busyHandler({ tries in > _Note:_ The default timeout is 0, so if you see `database is locked` errors, you may be trying to access the same database simultaneously from multiple connections. +### Connection Pools + +Connection pools use SQLite WAL mode to allow concurrent reads and writes, which can increase performance. Connection pools are created similar to connections: + +``` swift +let pool = try ConnectionPool("path/to/db.sqlite3") +``` + +Writes are done inside of a readWrite block: + +``` swift +pool.readWrite { connection in + try db.run(users.insert(email <- "alice@mac.com", name <- "Alice")) +} +``` + +Reads are done inside of a read block: + +``` swift +pool.read { connection in + for user in try db.prepare(users) { + print("id: \(user[id]), email: \(user[email]), name: \(user[name])") + } +} +``` + + ## Building Type-Safe SQL SQLite.swift comes with a typed expression layer that directly maps [Swift types](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/SwiftStandardLibraryReference/) to their [SQLite counterparts](https://www.sqlite.org/datatype3.html). From 66b61ddacc04576744decdafef6485f57f7a0d2e Mon Sep 17 00:00:00 2001 From: Nick Shelley Date: Tue, 19 Jul 2016 12:47:36 -0600 Subject: [PATCH 19/19] Use the right variable names. --- Documentation/Index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/Index.md b/Documentation/Index.md index 6ea09026..a33000e8 100644 --- a/Documentation/Index.md +++ b/Documentation/Index.md @@ -278,7 +278,7 @@ Writes are done inside of a readWrite block: ``` swift pool.readWrite { connection in - try db.run(users.insert(email <- "alice@mac.com", name <- "Alice")) + try connection.run(users.insert(email <- "alice@mac.com", name <- "Alice")) } ``` @@ -286,7 +286,7 @@ Reads are done inside of a read block: ``` swift pool.read { connection in - for user in try db.prepare(users) { + for user in try connection.prepare(users) { print("id: \(user[id]), email: \(user[email]), name: \(user[name])") } }