├── .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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
--------------------------------------------------------------------------------