diff --git a/Package.resolved b/Package.resolved index 862aec2..cbb2052 100644 --- a/Package.resolved +++ b/Package.resolved @@ -2,75 +2,66 @@ "object": { "pins": [ { - "package": "Console", - "repositoryURL": "https://github.com/vapor/console.git", + "package": "async-kit", + "repositoryURL": "https://github.com/0xLeif/async-kit.git", "state": { - "branch": null, - "revision": "74cfbea629d4aac34a97cead2447a6870af1950b", - "version": "3.1.1" + "branch": "master", + "revision": "01ae20f158bdc11711774467716ee552527a1fff", + "version": null } }, { - "package": "Core", - "repositoryURL": "https://github.com/vapor/core.git", + "package": "fluent-kit", + "repositoryURL": "https://github.com/0xLeif/fluent-kit.git", "state": { - "branch": null, - "revision": "10d33362a47fab03a067e78fb0791341d9c634fa", - "version": "3.9.3" + "branch": "master", + "revision": "8f2b1f7c9298b6952423dc16d1bfb77c9f295699", + "version": null } }, { - "package": "DatabaseKit", - "repositoryURL": "https://github.com/vapor/database-kit.git", + "package": "fluent-sqlite-driver", + "repositoryURL": "https://github.com/0xLeif/fluent-sqlite-driver.git", "state": { - "branch": null, - "revision": "8f352c8e66dab301ab9bfef912a01ce1361ba1e4", - "version": "1.3.3" + "branch": "master", + "revision": "59b058c305aca604b66b5e95bb32f6bde1205554", + "version": null } }, { - "package": "Fluent", - "repositoryURL": "https://github.com/vapor/fluent.git", + "package": "sql-kit", + "repositoryURL": "https://github.com/vapor/sql-kit.git", "state": { "branch": null, - "revision": "783819d8838d15e1a05b459aa0fd1bde1e37ac26", - "version": "3.2.1" + "revision": "8b82edd5d918068f204e3ac4206d5d15f6740395", + "version": "3.5.0" } }, { - "package": "FluentSQLite", - "repositoryURL": "https://github.com/vapor/fluent-sqlite-driver.git", + "package": "sqlite-kit", + "repositoryURL": "https://github.com/0xLeif/sqlite-kit.git", "state": { - "branch": null, - "revision": "c32f5bda84bf4ea691d19afe183d40044f579e11", - "version": "3.0.0" + "branch": "master", + "revision": "46bdd64c73925c6e0d5d30dbf621243442ee0b3e", + "version": null } }, { - "package": "Service", - "repositoryURL": "https://github.com/vapor/service.git", + "package": "sqlite-nio", + "repositoryURL": "https://github.com/vapor/sqlite-nio.git", "state": { - "branch": null, - "revision": "fa5b5de62bd68bcde9a69933f31319e46c7275fb", - "version": "1.0.2" + "branch": "master", + "revision": "af884d7f01f4dd93b2844c1f0dd2bf0fd24f04d3", + "version": null } }, { - "package": "SQL", - "repositoryURL": "https://github.com/vapor/sql.git", + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", "state": { "branch": null, - "revision": "50eaeb8f52a1ce63f1ff3880e1114dd8757a78a6", - "version": "2.3.2" - } - }, - { - "package": "SQLite", - "repositoryURL": "https://github.com/vapor/sqlite.git", - "state": { - "branch": null, - "revision": "314d9cd21165bcf14215e336a23ff8214f40e411", - "version": "3.2.1" + "revision": "57c6bd04256ba47590ee2285e208f731210c5c10", + "version": "1.3.0" } }, { @@ -78,17 +69,8 @@ "repositoryURL": "https://github.com/apple/swift-nio.git", "state": { "branch": null, - "revision": "8da5c5a4e6c5084c296b9f39dc54f00be146e0fa", - "version": "1.14.2" - } - }, - { - "package": "swift-nio-zlib-support", - "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", - "state": { - "branch": null, - "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", - "version": "1.0.0" + "revision": "8a865bd15e69526cbdfcfd7c47698eb20b2ba951", + "version": "2.19.0" } } ] diff --git a/Package.swift b/Package.swift index f88880f..ded14b1 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,8 @@ import PackageDescription let package = Package( name: "FLite", platforms: [ - .iOS("11.0") + .iOS("13.0"), + .macOS("10.15") ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. @@ -17,14 +18,24 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "3.0.0") + .package(url: "https://github.com/0xLeif/fluent-sqlite-driver.git", .branch("master")), + .package(url: "https://github.com/vapor/sqlite-nio.git", .branch("master")), + .package(url: "https://github.com/0xLeif/sqlite-kit.git", .branch("master")), + .package(url: "https://github.com/0xLeif/fluent-kit.git", .branch("master")), + .package(url: "https://github.com/0xLeif/async-kit.git", .branch("master")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "FLite", - dependencies: ["FluentSQLite"]), + dependencies: [ + .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), + .product(name: "SQLiteNIO", package: "sqlite-nio"), + .product(name: "SQLiteKit", package: "sqlite-kit"), + .product(name: "FluentKit", package: "fluent-kit"), + .product(name: "AsyncKit", package: "async-kit") + ]), .testTarget( name: "FLiteTests", dependencies: ["FLite"]), diff --git a/README.md b/README.md index 90ba24a..eedb766 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,40 @@ # FLite -FluentSQLite --> F + Lite -- > FLite +## FluentSQLiteDriver --> F + Lite -- > FLite -Example Use: +### Example Uses: +#### FLite.main ``` -FLite.storage = .memory +// Use FLite.main +// Default Storage Type: Memory -FLite.prepare(model: Todo.self) - -FLite.create(model: Todo(title: "Hello World")) +try? FLite.prepare(migration: Todo.self).wait() -FLite.fetch(model: Todo.self) { values in - print(values) +try! FLite.add(model: Todo(title: "Hello World", strings: ["hello", "world"])).wait() + +FLite.fetch(model: Todo.self) + .whenSuccess { (values) in + print(values) +} +``` + +#### FLite.init(...) +``` +// Create your own FLite + +let flite = FLite(threads: 30, + configuration: .sqlite(.memory, maxConnectionsPerEventLoop: 30), + id: .sqlite, + logger: Logger(label: "Custom.FLITE")) + +try? flite.prepare(migration: Todo.self).wait() + +try! flite.add(model: Todo(title: "Hello World", strings: ["hello", "world"])).wait() + +flite.fetch(model: Todo.self) + .whenSuccess { (values) in + print(values) } ``` diff --git a/Sources/FLite/FLite.swift b/Sources/FLite/FLite.swift index 0f4be3c..fffb00d 100644 --- a/Sources/FLite/FLite.swift +++ b/Sources/FLite/FLite.swift @@ -1,55 +1,147 @@ -import FluentSQLite +import FluentSQLiteDriver +import NIO -public struct FLite { - public static var storage: SQLiteStorage = .memory { - didSet { - FLite.manager = SQLiteDatabaseManager(storage: FLite.storage) +public class FLite { + // MARK: Private Values + + private var group: EventLoopGroup! + private var pool: NIOThreadPool! + private var dbs: Databases! + private var log: Logger! + + // MARK: deinit + + deinit { + shutdown() + } + + // MARK: Calculated Values + + private var db: Database { + dbs.database(logger: log, on: dbs.eventLoopGroup.next())! + } + + public static var main: FLite = FLite() + + // MARK: init + + // Private + + private init() { + let threads = System.coreCount + + group = MultiThreadedEventLoopGroup(numberOfThreads: threads) + + pool = .init(numberOfThreads: threads) + pool.start() + + dbs = Databases(threadPool: pool, on: group) + dbs.use(.sqlite(.memory), as: .sqlite) + + log = Logger(label: "FLITE") + } + + // Public + + public init(eventGroup: EventLoopGroup, + threadPool: NIOThreadPool, + configuration: DatabaseConfigurationFactory, + id: DatabaseID, + logger: Logger) { + group = eventGroup + pool = threadPool + + pool.start() + + dbs = Databases(threadPool: pool, on: group) + dbs.use(configuration, as: id) + + log = logger + } + + public init(threads: Int, + configuration: DatabaseConfigurationFactory, + id: DatabaseID, + logger: Logger) { + group = MultiThreadedEventLoopGroup(numberOfThreads: threads) + + pool = .init(numberOfThreads: threads) + pool.start() + + dbs = Databases(threadPool: pool, on: group) + dbs.use(configuration, as: id) + + log = logger + } + + // MARK: Database Functions + + public func prepare(migration: Migration) -> EventLoopFuture { + migration.prepare(on: db) + } + + public func prepare(migration: T.Type) -> EventLoopFuture { + migration.init().prepare(on: db) + } + + public func add(model: T) -> EventLoopFuture { + model.save(on: db) + } + + public func query(model: T.Type) -> QueryBuilder { + db.query(model) + } + + public func fetch(model: T.Type) -> EventLoopFuture<[T]> { + db.query(model).all() + } + + public func shutdown() { + dbs.shutdown() + dbs = nil + + do { + try pool.syncShutdownGracefully() + } catch { + pool.shutdownGracefully { + print("(NIOThreadPool) Shutting Down with Error: \($0.debugDescription)") + } + } + pool = nil + + do { + try group.syncShutdownGracefully() + } catch { + group.shutdownGracefully { + print("(EventLoopGroup) Shutting Down with Error: \($0.debugDescription)") + } } + group = nil } - public static var manager = SQLiteDatabaseManager(storage: FLite.storage) -} - -public extension FLite { - static func connection(withHandler handler: @escaping (SQLiteConnection) -> Void, - completionHandler completion: @escaping () -> Void) { - FLite.manager.connection(withHandler: handler, - completionHandler: completion) - } - - static func prepare(model: T.Type, - onError: @escaping (Error) -> () = { print($0) }, - onComplete: @escaping () -> Void) where T: SQLiteModel { - FLite.connection(withHandler: { (connection) in - model.prepare(on: connection) - .catchMap(onError) - .whenSuccess(onComplete) - }) {} - } - - static func create(model: T, - onError: @escaping (Error) -> () = { print($0) }, - onComplete: @escaping (T) -> Void) where T: SQLiteModel { - FLite.connection(withHandler: { (connection) in - model.save(on: connection) - .catch(onError) - .whenSuccess(onComplete) - }) {} - } - - static func fetchAll(model: T.Type, - onError: @escaping (Error) -> () = { print($0) }, - onComplete: @escaping ([T]) -> Void) where T: SQLiteModel { - FLite.connection(withHandler: { (connection) in - model.query(on: connection).all() - .catch(onError) - .whenSuccess(onComplete) - }) {} - } - - static func fetch(model: T.Type, - qb: @escaping (QueryBuilder) -> Void) where T: SQLiteModel { - FLite.manager.connection(withHandler: { (connection) in - qb(model.query(on: connection)) - }, completionHandler: {}) + + // MARK: Static Database Functions + + public static func prepare(migration: Migration) -> EventLoopFuture { + migration.prepare(on: FLite.main.db) + } + + public static func prepare(migration: T.Type) -> EventLoopFuture { + migration.init().prepare(on: FLite.main.db) + } + + public static func add(model: T) -> EventLoopFuture { + model.save(on: FLite.main.db) + } + + public static func query(model: T.Type) -> QueryBuilder { + FLite.main.db.query(model) + } + + public static func fetch(model: T.Type) -> EventLoopFuture<[T]> { + FLite.main.db.query(model).all() + } + + public static func shutdown() { + FLite.main.dbs.shutdown() } } diff --git a/Sources/FLite/SQLiteDatabaseManager.swift b/Sources/FLite/SQLiteDatabaseManager.swift deleted file mode 100644 index ec4ee46..0000000 --- a/Sources/FLite/SQLiteDatabaseManager.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SQLiteDatabaseManager.swift -// -// -// Created by 0xLeif on 2/27/20. -// - -import FluentSQLite - -public struct SQLiteDatabaseManager { - private let db: SQLiteDatabase - private let container: BasicContainer - private let databases: Databases - private var group: MultiThreadedEventLoopGroup - private var identifier: DatabaseIdentifier - private var config: DatabasesConfig - fileprivate var pool: DatabaseConnectionPool> - - public init(storage: SQLiteStorage = .memory, - id: DatabaseIdentifier = "default", - numberOfThreads: Int = 1, - maxConnections: Int = 10) { - self.db = try! SQLiteDatabase(storage: storage) - self.group = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads) - self.identifier = id - self.config = DatabasesConfig() - config.add(database: db, as: identifier) - self.container = BasicContainer(config: .init(), environment: .testing, services: .init(), on: group) - self.databases = try! config.resolve(on: container) - self.pool = try! databases.requireDatabase(for: identifier) - .newConnectionPool(config: .init(maxConnections: maxConnections), on: self.group) - } -} - -public extension SQLiteDatabaseManager { - func connection(withHandler handler: @escaping (SQLiteConnection) -> Void, - completionHandler completion: @escaping () -> Void) { - pool.requestConnection().do { - handler($0) - }.do { (connection) in - self.pool.releaseConnection(connection) - }.whenComplete { - completion() - } - } - - mutating func set(id: DatabaseIdentifier) { - self.identifier = id - } - - mutating func set(numberOfThreads: Int) { - self.group = MultiThreadedEventLoopGroup(numberOfThreads: numberOfThreads) - } - - - mutating func set(maxConnections: Int) { - self.pool = try! databases.requireDatabase(for: identifier) - .newConnectionPool(config: .init(maxConnections: maxConnections), on: self.group) - } -} diff --git a/Tests/FLiteTests/FLiteTests.swift b/Tests/FLiteTests/FLiteTests.swift index 34ffaa0..6cc2caa 100644 --- a/Tests/FLiteTests/FLiteTests.swift +++ b/Tests/FLiteTests/FLiteTests.swift @@ -1,104 +1,89 @@ import XCTest -import FluentSQLite +import FluentSQLiteDriver @testable import FLite - final class FLiteTests: XCTestCase { + override func tearDown() { + FLite.shutdown() + } + func testExample() { let semaphore = DispatchSemaphore(value: 0) var values = [Todo]() - FLite.storage = .memory + try? FLite.prepare(migration: Todo.self).wait() - FLite.prepare(model: Todo.self) { - "Prepared".log() - } + try! FLite.add(model: Todo(title: "Hello World", strings: ["hello", "world"])).wait() - FLite.create(model: Todo(title: "Hello World", strings: ["hello", "world"])) { - "Created: \($0)".log() - } - - FLite.fetch(model: Todo.self) { qb in - qb.all() - .whenSuccess { - "Values: \($0)".log() - values = $0 - semaphore.signal() - } + FLite.fetch(model: Todo.self) + .whenSuccess { (todos) in + values = todos + semaphore.signal() } semaphore.wait() XCTAssert(values.count > 0) } - func testTodoArray() { + func testCustomFLite() { let semaphore = DispatchSemaphore(value: 0) - var values = (0 ..< 500).map { _ in Todo(title: "Todo #\(Int.random(in: 0 ... 10000))", strings: []) } + var values = [Todo]() - FLite.storage = .memory + let flite = FLite(threads: 30, + configuration: .sqlite(.memory, maxConnectionsPerEventLoop: 30), + id: .sqlite, + logger: Logger(label: "Custom.FLITE")) - FLite.manager.set(maxConnections: 100) + try? flite.prepare(migration: Todo.self).wait() - FLite.manager.set(id: "Something Else") + try! flite.add(model: Todo(title: "Hello World", strings: ["hello", "world"])).wait() - FLite.prepare(model: Todo.self) { - "Prepared".log() + flite.fetch(model: Todo.self) + .whenSuccess { (todos) in + values = todos + semaphore.signal() } - values.forEach { value in - FLite.create(model: value) { - "Created: \($0)".log() - } + semaphore.wait() + XCTAssert(values.count > 0) + } + + func testTodoArray() { + let semaphore = DispatchSemaphore(value: 0) + var values = (0 ..< 500).map { _ in Todo(title: "Todo #\(Int.random(in: 0 ... 10000))", strings: []) } + + try? FLite.prepare(migration: Todo.self).wait() + + values.forEach { value in + try! FLite.add(model: value).wait() } - FLite.fetch(model: Todo.self) { qb in - qb.all() - .whenSuccess { - "Value: \($0)".log() - values = $0 - semaphore.signal() - } + FLite.fetch(model: Todo.self) + .whenSuccess { (todos) in + values = todos + semaphore.signal() } semaphore.wait() XCTAssert(values.count > 0) + XCTAssertEqual(values.count, 500) } func testTodoList_big() { let semaphore = DispatchSemaphore(value: 0) var bigList: TodoList? - FLite.storage = .memory - - FLite.manager.set(maxConnections: 10) - - FLite.manager.set(id: "Something Else") - - FLite.prepare(model: Todo.self) { - "Prepared".log() - } - - FLite.prepare(model: TodoList.self) { - "TodoList Ready".log() - } + try? FLite.prepare(migration: Todo.self).wait() + try? FLite.prepare(migration: TodoList.self).wait() let list = TodoList(title: "First", items: (0 ..< 100_000).map { _ in Todo(title: "Todo #\(Int.random(in: 0 ... 100_000))", strings: ["1", "two", "111"]) }) - FLite.create(model: list) { (value) in - "Created: \(value)".log() - } - - FLite.create(model: TodoList(title: "BIG", items: (0 ..< 1_000_000).map { _ in Todo(title: "Todo #\(Int.random(in: 0 ... 1_000_000))", strings: []) })) { - "Created: \($0)".log() - } + try! FLite.add(model: list).wait() + try! FLite.add(model: TodoList(title: "BIG", items: (0 ..< 1_000_000).map { _ in Todo(title: "Todo #\(Int.random(in: 0 ... 1_000_000))", strings: []) })).wait() - FLite.fetch(model: TodoList.self) { qb in - qb.filter(\.title == "BIG") - .first() - .whenSuccess { - bigList = $0 - semaphore.signal() - } + FLite.query(model: TodoList.self).filter("title", .equal, "BIG").first().whenSuccess { + bigList = $0 + semaphore.signal() } semaphore.wait() diff --git a/Tests/FLiteTests/Helpers/TestHelperExtensions.swift b/Tests/FLiteTests/Helpers/TestHelperExtensions.swift deleted file mode 100644 index cd14658..0000000 --- a/Tests/FLiteTests/Helpers/TestHelperExtensions.swift +++ /dev/null @@ -1,8 +0,0 @@ -internal let shouldLog = false -internal extension String { - func log() { - if shouldLog { - print(self) - } - } -} diff --git a/Tests/FLiteTests/Models/Todo.swift b/Tests/FLiteTests/Models/Todo.swift index 89c0e0e..d079783 100644 --- a/Tests/FLiteTests/Models/Todo.swift +++ b/Tests/FLiteTests/Models/Todo.swift @@ -1,16 +1,24 @@ -import FluentSQLite -/// A single entry of a Todo list. -internal final class Todo: SQLiteModel { +import Foundation +import FluentSQLiteDriver + +internal final class Todo: Model { + init() { } + + static let schema: String = "todos" + /// The unique identifier for this `Todo`. - var id: Int? + @ID(key: .id) + var id: UUID? /// A title describing what this `Todo` entails. + @Field(key: "title") var title: String + @Field(key: "someList") var someList: [String] /// Creates a new `Todo`. - init(id: Int? = nil, title: String, strings: [String]) { + init(id: UUID? = nil, title: String, strings: [String]) { self.id = id self.title = title self.someList = strings @@ -18,11 +26,23 @@ internal final class Todo: SQLiteModel { } /// Allows `Todo` to be used as a dynamic migration. -extension Todo: Migration { } -extension Todo: CustomStringConvertible { +extension Todo: Migration { + func prepare(on database: Database) -> EventLoopFuture { + database.schema(Todo.schema) + .id() + .field("title", .string, .required) + .field("someList", .array(of: .string), .required) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + database.schema(Todo.schema).delete() + } +} +extension Todo: CustomStringConvertible { var description: String { return """ - Todo id: \(id ?? -1) + Todo id: \(id) title: \(title) someList: \(someList) """ @@ -30,15 +50,22 @@ extension Todo: CustomStringConvertible { } // A Todo list. -internal final class TodoList: SQLiteModel { - var id: Int? - +internal final class TodoList: Model { + init() { } + + static var schema: String = "todolist" + + @ID(key: .id) + var id: UUID? + + @Field(key: "title") var title: String + @Field(key: "items") var items: [Todo] /// Creates a new `Todo`. - init(id: Int? = nil, title: String, items: [Todo]) { + init(id: UUID? = nil, title: String, items: [Todo]) { self.id = id self.title = title self.items = items @@ -46,11 +73,24 @@ internal final class TodoList: SQLiteModel { } /// Allows `TodoList` to be used as a dynamic migration. -extension TodoList: Migration { } +extension TodoList: Migration { + func prepare(on database: Database) -> EventLoopFuture { + database.schema(TodoList.schema) + .id() + .field("title", .string, .required) + .field("items", .array(of: .custom(Todo.self)), .required) + .create() + } + + func revert(on database: Database) -> EventLoopFuture { + database.schema(TodoList.schema).delete() + } +} + extension TodoList: CustomStringConvertible { var description: String { return """ - TodoList id: \(id ?? -1) + TodoList id: \(id) title: \(title) items: \(items) """