├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── DatabaseKit │ ├── Connection │ └── DatabaseConnection.swift │ ├── Database │ ├── Database.swift │ ├── DatabaseIdentifier.swift │ ├── Databases.swift │ └── ThreadConnectionPool.swift │ └── Utilities │ ├── DatabaseKitError.swift │ ├── Deprecated.swift │ ├── Exports.swift │ └── URL+DatabaseName.swift ├── Tests ├── .DS_Store ├── DatabaseKitTests │ ├── .DS_Store │ └── SQLiteBenchmarkTests.swift └── LinuxMain.swift └── circle.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | *.xcodeproj 3 | Package.resolved 4 | .DS_Store 5 | DerivedData 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Qutheory, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "database-kit", 6 | products: [ 7 | .library(name: "DatabaseKit", targets: ["DatabaseKit"]), 8 | ], 9 | dependencies: [ 10 | .package(url: "https://github.com/vapor/nio-kit.git", .branch("master")), 11 | ], 12 | targets: [ 13 | .target(name: "DatabaseKit", dependencies: ["NIOKit"]), 14 | .testTarget(name: "DatabaseKitTests", dependencies: ["DatabaseKit"]), 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Database Kit 3 |
4 |
5 | 6 | Documentation 7 | 8 | 9 | Team Chat 10 | 11 | 12 | MIT License 13 | 14 | 15 | Continuous Integration 16 | 17 | 18 | Swift 4.1 19 | 20 |

21 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Connection/DatabaseConnection.swift: -------------------------------------------------------------------------------- 1 | /// Types conforming to this protocol can be used as a `Database.Connection`. 2 | /// 3 | /// Most of the database interaction work is done through static methods on `Database` that accept 4 | /// a connection. However, there are a few things like `isClosed` and `close()` that a connection must implement. 5 | public protocol DatabaseConnection: ConnectionPoolItem { 6 | /// This connection's event loop. 7 | var eventLoop: EventLoop { get } 8 | 9 | /// Closes the `DatabaseConnection`. 10 | func close() -> EventLoopFuture 11 | } 12 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Database/Database.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | /// Types conforming to this protocol can be used as a database for connections and connection pools. 4 | /// 5 | /// This protocol is the basis for `...Supporting` protocols that further extend it, such as `KeyedCacheSupporting`. 6 | public protocol Database: ConnectionPoolSource where Connection: DatabaseConnection { } 7 | 8 | extension Database { 9 | /// Creates a new `DatabaseConnectionPool` for this `Database`. 10 | /// 11 | /// let pool = try database.newConnectionPool(config: .default(), on: ...).wait() 12 | /// let conn = try pool.requestConnection().wait() 13 | /// 14 | /// - paramters: 15 | /// - config: `DatabaseConnectionPoolConfig` that will be used to create the actual pool. 16 | /// - worker: `Worker` to perform async work on. 17 | /// - returns: Newly created `DatabaseConnectionPool`. 18 | public func makeConnectionPool(config: ConnectionPoolConfig) -> ConnectionPool { 19 | return ConnectionPool(config: config, source: self) 20 | } 21 | } 22 | 23 | 24 | extension Database { 25 | public func withConnection(closure: @escaping (Connection) -> EventLoopFuture) -> EventLoopFuture { 26 | return self.makeConnection().flatMap { conn -> EventLoopFuture in 27 | return closure(conn).flatMap { result in 28 | return conn.close().map { result } 29 | }.flatMapError { error in 30 | return conn.close().flatMapThrowing { throw error } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Database/DatabaseIdentifier.swift: -------------------------------------------------------------------------------- 1 | /// Each database in your application receives its own identifier. 2 | /// 3 | /// Create identifiers for your non-default databases by adding 4 | /// a static extension to this struct: 5 | /// 6 | /// extension DatabaseIdentifier { 7 | /// /// My custom DB. 8 | /// public static var myCustom: DatabaseIdentifier { 9 | /// return DatabaseIdentifier("foo-custom") 10 | /// } 11 | /// } 12 | /// 13 | public struct DatabaseIdentifier: Equatable, Hashable, CustomStringConvertible, ExpressibleByStringLiteral { 14 | /// The unique id. 15 | public let uid: String 16 | 17 | /// See `CustomStringConvertible`. 18 | public var description: String { 19 | return uid 20 | } 21 | 22 | /// Create a new `DatabaseIdentifier`. 23 | public init(_ uid: String) { 24 | self.uid = uid 25 | } 26 | 27 | /// See `ExpressibleByStringLiteral`. 28 | public init(stringLiteral value: String) { 29 | self.init(value) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Database/Databases.swift: -------------------------------------------------------------------------------- 1 | /// Helper struct for configuring the `Databases` struct. 2 | public struct Databases { 3 | /// The configured databases. 4 | private var storage: [String: Any] 5 | 6 | /// Create a new database config helper. 7 | public init() { 8 | self.storage = [:] 9 | } 10 | 11 | // MARK: Database 12 | 13 | /// Add a pre-initialized database to the config. 14 | /// 15 | /// let psql: PostgreSQLDatabase = ... 16 | /// databases.add(database: psql, as: .psql) 17 | /// 18 | /// - parameters: 19 | /// - database: Initialized instance of a `Database` to add. 20 | /// - id: `DatabaseIdentifier` to use for this `Database`. 21 | public mutating func add(database: D, as id: DatabaseIdentifier) { 22 | assert(self.storage[id.uid] == nil, "A database with id '\(id.uid)' is already registered.") 23 | self.storage[id.uid] = database 24 | } 25 | 26 | /// Fetches the `Database` for a given `DatabaseIdentifier`. 27 | /// 28 | /// let psql = try databases.requireDatabase(for: .psql) 29 | /// 30 | /// - parameters: 31 | /// - id: `DatabaseIdentifier` of the `Database` to fetch. 32 | /// - throws: Throws an error if no `Database` with that id was found. 33 | /// - returns: `Database` identified by the supplied ID. 34 | public func requireDatabase(for dbid: DatabaseIdentifier) throws -> D { 35 | guard let db = database(for: dbid) else { 36 | throw DatabaseKitError(identifier: "dbRequired", reason: "No database with id '\(dbid.uid)' is configured.") 37 | } 38 | return db 39 | } 40 | 41 | /// Fetches the `Database` for a given `DatabaseIdentifier`. 42 | /// 43 | /// let psql = databases.database(for: .psql) 44 | /// 45 | /// - parameters: 46 | /// - id: `DatabaseIdentifier` of the `Database` to fetch. 47 | /// - returns: `Database` identified by the supplied ID, if one could be found. 48 | public func database(for dbid: DatabaseIdentifier) -> D? { 49 | guard let db = self.storage[dbid.uid] as? D else { 50 | return nil 51 | } 52 | return db 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Database/ThreadConnectionPool.swift: -------------------------------------------------------------------------------- 1 | public struct ThreadConnectionPool { 2 | public let databases: Databases 3 | public let config: ConnectionPoolConfig 4 | 5 | private final class PoolStorage { 6 | var pools: [String: Any] 7 | init() { 8 | self.pools = [:] 9 | } 10 | } 11 | 12 | private var poolStorage = ThreadSpecificVariable() 13 | 14 | public init(databases: Databases, config: ConnectionPoolConfig) { 15 | self.databases = databases 16 | self.config = config 17 | } 18 | 19 | public func get(for dbid: DatabaseIdentifier) throws -> ConnectionPool { 20 | if let storage = self.poolStorage.currentValue { 21 | if let pool = storage.pools[dbid.uid] as? ConnectionPool { 22 | return pool 23 | } else { 24 | let pool = try self.databases.requireDatabase(for: dbid) 25 | .makeConnectionPool(config: config) 26 | storage.pools[dbid.uid] = pool 27 | return pool 28 | } 29 | } else { 30 | let storage = PoolStorage() 31 | self.poolStorage.currentValue = storage 32 | let pool = try self.databases.requireDatabase(for: dbid) 33 | .makeConnectionPool(config: config) 34 | storage.pools[dbid.uid] = pool 35 | return pool 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Utilities/DatabaseKitError.swift: -------------------------------------------------------------------------------- 1 | /// Errors that can be thrown while working with Vapor. 2 | public struct DatabaseKitError: Error { 3 | #warning("use enum?") 4 | /// See `Debuggable`. 5 | public static let readableName = "DatabaseKit Error" 6 | 7 | /// See `Debuggable`. 8 | public let identifier: String 9 | 10 | /// See `Debuggable`. 11 | public var reason: String 12 | 13 | /// See `Debuggable`. 14 | public var suggestedFixes: [String] 15 | 16 | /// See `Debuggable`. 17 | public var possibleCauses: [String] 18 | 19 | /// Creates a new `DatabaseKitError`. 20 | init( 21 | identifier: String, 22 | reason: String, 23 | suggestedFixes: [String] = [], 24 | possibleCauses: [String] = [], 25 | file: String = #file, 26 | function: String = #function, 27 | line: UInt = #line, 28 | column: UInt = #column 29 | ) { 30 | self.identifier = identifier 31 | self.reason = reason 32 | self.suggestedFixes = suggestedFixes 33 | self.possibleCauses = possibleCauses 34 | } 35 | } 36 | 37 | /// For printing un-handleable errors. 38 | func ERROR(_ string: @autoclosure () -> String, file: StaticString = #file, line: Int = #line) { 39 | print("[DatabaseKit] \(string()) [\(file.description.split(separator: "/").last!):\(line)]") 40 | } 41 | 42 | /// Only includes the supplied closure in non-release builds. 43 | internal func debugOnly(_ body: () -> Void) { 44 | assert({ body(); return true }()) 45 | } 46 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Utilities/Deprecated.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | 3 | /// nothing here yet... 4 | 5 | extension Databases { 6 | @available(*, unavailable, message: "Register a database instance instead.") 7 | public mutating func add(database: D.Type, as id: DatabaseIdentifier) { 8 | fatalError() 9 | } 10 | 11 | @available(*, unavailable, message: "Register a database instance instead.") 12 | public mutating func add(as id: DatabaseIdentifier, database: @escaping (Any) throws -> D) { 13 | fatalError() 14 | } 15 | 16 | @available(*, unavailable, renamed: "resolve") 17 | public func resolve(on container: Any) throws -> Databases { 18 | fatalError() 19 | } 20 | 21 | @available(*, unavailable, message: "Enable logging in database specific config instead.") 22 | public mutating func enableLogging(on db: DatabaseIdentifier, logger: Any) { 23 | fatalError() 24 | } 25 | 26 | @available(*, unavailable, message: "Use database specific config instead.") 27 | public mutating func appendConfigurationHandler(on db: DatabaseIdentifier, _ configure: @escaping (D.Connection) -> EventLoopFuture) { 28 | fatalError() 29 | } 30 | } 31 | 32 | @available(*, unavailable, renamed: "Databases") 33 | public typealias DatabasesConfig = Databases 34 | 35 | @available(*, unavailable, renamed: "ConnectionPool") 36 | public typealias DatabaseConnectionPool = Any 37 | 38 | @available(*, unavailable, renamed: "ConnectionPoolConfig") 39 | public typealias DatabaseConnectionPoolConfig = Any 40 | 41 | @available(*, unavailable, message: "Use ORM-specific database protocol instead.") 42 | public typealias DatabaseQueryable = Any 43 | 44 | @available(*, unavailable, message: "Use ORM-specific database protocol instead.") 45 | public typealias DatabaseConnectable = Any 46 | 47 | @available(*, unavailable) 48 | public typealias DatabaseStringFindable = Any 49 | 50 | @available(*, unavailable, message: "Enable logging in database specific config instead.") 51 | public typealias LogSupporting = Any 52 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Utilities/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import NIOKit 2 | -------------------------------------------------------------------------------- /Sources/DatabaseKit/Utilities/URL+DatabaseName.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Adds conveniences for using URLs referencing database servers. 4 | extension URL { 5 | /// If present, this specifies a database on database servers that support multiple databases. 6 | /// 7 | /// This property converts the URL's `path` property to a database name by removing the leading `/` 8 | /// and all data after trailing `/`. 9 | /// 10 | /// URL(string: "/vapor_database/asdf")?.databaseName // "vapor_database" 11 | /// 12 | public var databaseName: String? { 13 | return path.split(separator: "/", omittingEmptySubsequences: true).first.flatMap(String.init) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapor/database-kit/6f7099a9cde284b4041f544a1d5e16669300434f/Tests/.DS_Store -------------------------------------------------------------------------------- /Tests/DatabaseKitTests/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vapor/database-kit/6f7099a9cde284b4041f544a1d5e16669300434f/Tests/DatabaseKitTests/.DS_Store -------------------------------------------------------------------------------- /Tests/DatabaseKitTests/SQLiteBenchmarkTests.swift: -------------------------------------------------------------------------------- 1 | import DatabaseKit 2 | import XCTest 3 | import NIO 4 | 5 | final class DatabaseKitTests: XCTestCase { 6 | func testURLDatabaseName() { 7 | XCTAssertEqual(URL(string: "/vapor_database/asdf")?.databaseName, "vapor_database") 8 | } 9 | 10 | func testConnectionPooling() throws { 11 | let foo = FooDatabase(on: EmbeddedEventLoop()) 12 | let pool = foo.makeConnectionPool(config: .init(maxConnections: 2)) 13 | 14 | // make two connections 15 | let connA = try pool.requestConnection().wait() 16 | XCTAssertEqual(connA.isClosed, false) 17 | let connB = try pool.requestConnection().wait() 18 | XCTAssertEqual(connB.isClosed, false) 19 | XCTAssertEqual(foo.connectionsCreated, 2) 20 | 21 | // try to make a third, but pool only supports 2 22 | var connC: FooConnection? 23 | pool.requestConnection().whenSuccess { connC = $0 } 24 | XCTAssertNil(connC) 25 | XCTAssertEqual(foo.connectionsCreated, 2) 26 | 27 | // release one of the connections, allowing the third to be made 28 | pool.releaseConnection(connB) 29 | XCTAssertNotNil(connC) 30 | XCTAssert(connC === connB) 31 | XCTAssertEqual(foo.connectionsCreated, 2) 32 | 33 | // try to make a third again, with two active 34 | var connD: FooConnection? 35 | pool.requestConnection().whenSuccess { connD = $0 } 36 | XCTAssertNil(connD) 37 | XCTAssertEqual(foo.connectionsCreated, 2) 38 | 39 | // this time, close the connection before releasing it 40 | try connC!.close().wait() 41 | pool.releaseConnection(connC!) 42 | XCTAssert(connD !== connB) 43 | XCTAssertEqual(connD?.isClosed, false) 44 | XCTAssertEqual(foo.connectionsCreated, 3) 45 | } 46 | 47 | func testConnectionPoolPerformance() { 48 | let foo = FooDatabase(on: EmbeddedEventLoop()) 49 | let pool = foo.makeConnectionPool(config: .init(maxConnections: 10)) 50 | 51 | measure { 52 | for _ in 0..<10_000 { 53 | do { 54 | let connA = try! pool.requestConnection().wait() 55 | pool.releaseConnection(connA) 56 | } 57 | do { 58 | let connA = try! pool.requestConnection().wait() 59 | let connB = try! pool.requestConnection().wait() 60 | let connC = try! pool.requestConnection().wait() 61 | pool.releaseConnection(connB) 62 | pool.releaseConnection(connC) 63 | pool.releaseConnection(connA) 64 | } 65 | do { 66 | let connA = try! pool.requestConnection().wait() 67 | let connB = try! pool.requestConnection().wait() 68 | pool.releaseConnection(connA) 69 | pool.releaseConnection(connB) 70 | } 71 | } 72 | } 73 | } 74 | 75 | static let allTests = [ 76 | ("testURLDatabaseName", testURLDatabaseName), 77 | ("testConnectionPooling", testConnectionPooling), 78 | ("testConnectionPoolPerformance", testConnectionPoolPerformance), 79 | ] 80 | } 81 | 82 | // MARK: Private 83 | 84 | private final class FooDatabase: Database { 85 | typealias Connection = FooConnection 86 | 87 | var connectionsCreated: Int 88 | var eventLoop: EventLoop 89 | 90 | init(on eventLoop: EventLoop) { 91 | self.eventLoop = eventLoop 92 | self.connectionsCreated = 0 93 | } 94 | 95 | func makeConnection() -> EventLoopFuture { 96 | connectionsCreated += 1 97 | return self.eventLoop.makeSucceededFuture(FooConnection(on: self.eventLoop)) 98 | } 99 | } 100 | 101 | private final class FooConnection: DatabaseConnection { 102 | var isClosed: Bool 103 | let eventLoop: EventLoop 104 | 105 | init(on eventLoop: EventLoop) { 106 | self.isClosed = false 107 | self.eventLoop = eventLoop 108 | } 109 | 110 | func close() -> EventLoopFuture { 111 | self.isClosed = true 112 | return self.eventLoop.makeSucceededFuture(()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | 3 | import XCTest 4 | @testable import DatabaseKitTests 5 | 6 | XCTMain([ 7 | // DatabaseKit 8 | testCase(DatabaseKitTests.allTests), 9 | ]) 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | linux: 5 | docker: 6 | - image: codevapor/swift:5.0 7 | steps: 8 | - checkout 9 | - run: swift build 10 | - run: swift test 11 | linux-release: 12 | docker: 13 | - image: codevapor/swift:5.0 14 | steps: 15 | - checkout 16 | - run: swift build -c release 17 | workflows: 18 | version: 2 19 | tests: 20 | jobs: 21 | - linux 22 | - linux-release 23 | --------------------------------------------------------------------------------