diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index bcaf11e..0000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,34 +0,0 @@ -# Contributing to Fluent's SQLite Driver - -👋 Welcome to the Vapor team! - -## Xcode - -To open the project in Xcode: - -- Clone the repo to your computer -- Drag and drop the folder onto Xcode - -To test within Xcode, press `CMD+U`. - -## SPM - -To develop using SPM, open the code in your favorite code editor. Use the following commands from within the project's root folder to build and test. - -```sh -swift build -swift test -``` - -## SemVer - -Vapor follows [SemVer](https://semver.org). This means that any changes to the source code that can cause -existing code to stop compiling _must_ wait until the next major version to be included. - -Code that is only additive and will not break any existing code can be included in the next minor release. - ----------- - -Join us on Discord if you have any questions: [vapor.team](http://vapor.team). - -— Thanks! 🙌 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 299cb50..998a0eb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,4 @@ version: 2 -enable-beta-ecosystems: true updates: - package-ecosystem: "github-actions" directory: "/" @@ -9,12 +8,3 @@ updates: dependencies: patterns: - "*" - - package-ecosystem: "swift" - directory: "/" - schedule: - interval: "daily" - groups: - dependencies: - patterns: - - "*" - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d8c4c6..eaee569 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,5 +9,4 @@ on: jobs: unit-tests: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main - with: - with_tsan: false + secrets: inherit diff --git a/Package.swift b/Package.swift index 8c96fa2..dd1138b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 import PackageDescription let package = Package( @@ -13,20 +13,33 @@ let package = Package( .library(name: "FluentSQLiteDriver", targets: ["FluentSQLiteDriver"]), ], dependencies: [ - .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.45.0"), - .package(url: "https://github.com/vapor/sqlite-kit.git", from: "4.4.1"), - .package(url: "https://github.com/apple/swift-log.git", from: "1.5.3"), + .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.3"), + .package(url: "https://github.com/vapor/sqlite-kit.git", from: "4.5.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), ], targets: [ - .target(name: "FluentSQLiteDriver", dependencies: [ - .product(name: "FluentKit", package: "fluent-kit"), - .product(name: "FluentSQL", package: "fluent-kit"), - .product(name: "Logging", package: "swift-log"), - .product(name: "SQLiteKit", package: "sqlite-kit"), - ]), - .testTarget(name: "FluentSQLiteDriverTests", dependencies: [ - .product(name: "FluentBenchmark", package: "fluent-kit"), - .target(name: "FluentSQLiteDriver"), - ]), + .target( + name: "FluentSQLiteDriver", + dependencies: [ + .product(name: "FluentKit", package: "fluent-kit"), + .product(name: "FluentSQL", package: "fluent-kit"), + .product(name: "Logging", package: "swift-log"), + .product(name: "SQLiteKit", package: "sqlite-kit"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "FluentSQLiteDriverTests", + dependencies: [ + .product(name: "FluentBenchmark", package: "fluent-kit"), + .target(name: "FluentSQLiteDriver"), + ], + swiftSettings: swiftSettings + ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), +] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 0000000..9131fba --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,48 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "fluent-sqlite-driver", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v6), + .tvOS(.v13), + ], + products: [ + .library(name: "FluentSQLiteDriver", targets: ["FluentSQLiteDriver"]), + ], + dependencies: [ + .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.3"), + .package(url: "https://github.com/vapor/sqlite-kit.git", from: "4.5.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + ], + targets: [ + .target( + name: "FluentSQLiteDriver", + dependencies: [ + .product(name: "FluentKit", package: "fluent-kit"), + .product(name: "FluentSQL", package: "fluent-kit"), + .product(name: "Logging", package: "swift-log"), + .product(name: "SQLiteKit", package: "sqlite-kit"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "FluentSQLiteDriverTests", + dependencies: [ + .product(name: "FluentBenchmark", package: "fluent-kit"), + .target(name: "FluentSQLiteDriver"), + ], + swiftSettings: swiftSettings + ), + ] +) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/README.md b/README.md index 23dfea6..8f390b1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,35 @@

- - - FluentSQLite - + + + FluentSQLiteDriver +

-Documentation -Team Chat -MIT License -Continuous Integration - -Swift 5.7+ +Documentation +Team Chat +MIT License +Continuous Integration + +Swift 5.8+

+
+ +FluentSQLiteDriver is a [FluentKit] driver for SQLite clients. It provides support for using the Fluent ORM with SQLite databases, and uses [SQLiteKit] to provide [SQLKit] driver services, [SQLiteNIO] to connect and communicate databases asynchronously, and [AsyncKit] to provide connection pooling. + +[FluentKit]: https://github.com/vapor/fluent-kit +[SQLKit]: https://github.com/vapor/sql-kit +[SQLiteKit]: https://github.com/vapor/sqlite-kit +[SQLiteNIO]: https://github.com/vapor/sqlite-nio +[AsyncKit]: https://github.com/vapor/async-kit + +### Usage + +Use the SPM string to easily include the dependendency in your `Package.swift` file: + +```swift +.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0") +``` + +For additional information, see [the Fluent documentation](https://docs.vapor.codes/fluent/overview/). diff --git a/Sources/FluentSQLiteDriver/Docs.docc/Resources/vapor-fluentsqlitedriver-logo.svg b/Sources/FluentSQLiteDriver/Docs.docc/Resources/vapor-fluentsqlitedriver-logo.svg new file mode 100644 index 0000000..9cd158a --- /dev/null +++ b/Sources/FluentSQLiteDriver/Docs.docc/Resources/vapor-fluentsqlitedriver-logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/Sources/FluentSQLiteDriver/Docs.docc/index.md b/Sources/FluentSQLiteDriver/Docs.docc/index.md index 7479b25..c7a4e35 100644 --- a/Sources/FluentSQLiteDriver/Docs.docc/index.md +++ b/Sources/FluentSQLiteDriver/Docs.docc/index.md @@ -1,3 +1,14 @@ # ``FluentSQLiteDriver`` -FluentSQLiteDriver is a package to integrate SQLiteNIO and and SQLiteKit with FluentKit to make it easy to use and write SQLite database operations in Swift. \ No newline at end of file +FluentSQLiteDriver is a [FluentKit] driver for SQLite clients. + +## Overview + +FluentSQLiteDriver provides support for using the Fluent ORM with SQLite databases. It uses [SQLiteKit] to provide [SQLKit] driver services, [SQLiteNIO] to connect and communicate with databases asynchronously, and [AsyncKit] to provide connection pooling. + +[FluentKit]: https://github.com/vapor/fluent-kit +[SQLKit]: https://github.com/vapor/sql-kit +[SQLiteKit]: https://github.com/vapor/sqlite-kit +[SQLiteNIO]: https://github.com/vapor/sqlite-nio +[AsyncKit]: https://github.com/vapor/async-kit + diff --git a/Sources/FluentSQLiteDriver/Docs.docc/theme-settings.json b/Sources/FluentSQLiteDriver/Docs.docc/theme-settings.json new file mode 100644 index 0000000..bfcc1df --- /dev/null +++ b/Sources/FluentSQLiteDriver/Docs.docc/theme-settings.json @@ -0,0 +1,21 @@ +{ + "theme": { + "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, + "border-radius": "0", + "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, + "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, + "color": { + "fluentsqlitedriver": "hsl(215, 45%, 58%)", + "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentsqlitedriver) 30%, #000 100%)", + "documentation-intro-accent": "var(--color-fluentsqlitedriver)", + "logo-base": { "dark": "#fff", "light": "#000" }, + "logo-shape": { "dark": "#000", "light": "#fff" }, + "fill": { "dark": "#000", "light": "#fff" } + }, + "icons": { "technology": "/fluentsqlitedriver/images/vapor-fluentsqlitedriver-logo.svg" } + }, + "features": { + "quickNavigation": { "enable": true }, + "i18n": { "enable": true } + } +} diff --git a/Sources/FluentSQLiteDriver/Exports.swift b/Sources/FluentSQLiteDriver/Exports.swift index 16db801..e0348df 100644 --- a/Sources/FluentSQLiteDriver/Exports.swift +++ b/Sources/FluentSQLiteDriver/Exports.swift @@ -1,17 +1,8 @@ -#if swift(>=5.8) - @_documentation(visibility: internal) @_exported import FluentKit @_documentation(visibility: internal) @_exported import SQLiteKit -#else - -@_exported import FluentKit -@_exported import SQLiteKit - -#endif - extension DatabaseID { public static var sqlite: DatabaseID { - return .init(string: "sqlite") + .init(string: "sqlite") } } diff --git a/Sources/FluentSQLiteDriver/FluentSQLiteConfiguration.swift b/Sources/FluentSQLiteDriver/FluentSQLiteConfiguration.swift index 6aa3f5e..373641b 100644 --- a/Sources/FluentSQLiteDriver/FluentSQLiteConfiguration.swift +++ b/Sources/FluentSQLiteDriver/FluentSQLiteConfiguration.swift @@ -2,19 +2,83 @@ import NIO import FluentKit import SQLiteKit import AsyncKit +import Logging + +// Hint: Yes, I know what default arguments are. This ridiculous spelling out of each alternative avoids public API +// breakage from adding the defaults. And yes, `maxConnectionsPerEventLoop` is not forwarded on purpose, it's not +// an oversight or an omission. We no longer support it for SQLite because increasing it past one causes thread +// conntention but can never increase parallelism. extension DatabaseConfigurationFactory { + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. + public static func sqlite(_ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10)) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: .init(), sqlLogLevel: .debug) + } + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. + public static func sqlite( + _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), dataEncoder: SQLiteDataEncoder + ) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: dataEncoder, dataDecoder: .init(), sqlLogLevel: .debug) + } + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. + public static func sqlite( + _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), dataDecoder: SQLiteDataDecoder + ) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: dataDecoder, sqlLogLevel: .debug) + } + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. + public static func sqlite( + _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), + dataEncoder: SQLiteDataEncoder, dataDecoder: SQLiteDataDecoder + ) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: dataEncoder, dataDecoder: dataDecoder, sqlLogLevel: .debug) + } + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. public static func sqlite( - _ configuration: SQLiteConfiguration = .init(storage: .memory), + _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), sqlLogLevel: Logger.Level? + ) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: .init(), sqlLogLevel: sqlLogLevel) + } + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. + public static func sqlite( + _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), + dataEncoder: SQLiteDataEncoder, sqlLogLevel: Logger.Level? + ) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: dataEncoder, dataDecoder: .init(), sqlLogLevel: sqlLogLevel) + } + /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. + public static func sqlite( + _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), + dataDecoder: SQLiteDataDecoder, sqlLogLevel: Logger.Level? + ) -> Self { + self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: dataDecoder, sqlLogLevel: sqlLogLevel) + } + + /// Return a configuration factory using the provided parameters. + /// + /// - Parameters: + /// - configuration: The underlying `SQLiteConfiguration`. + /// - maxConnnectionsPerEventLoop: Ignored. The value is always treated as 1. + /// - dataEncoder: An ``SQLiteDataEncoder`` used to translate bound query parameters into `SQLiteData` values. + /// - dataDecoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values. + /// - queryLogLevel: The level at which SQL queries issued through the Fluent or SQLKit interfaces will be logged. + /// - Returns: A configuration factory, + public static func sqlite( + _ configuration: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, - connectionPoolTimeout: NIO.TimeAmount = .seconds(10) + connectionPoolTimeout: TimeAmount = .seconds(10), + dataEncoder: SQLiteDataEncoder, + dataDecoder: SQLiteDataDecoder, + sqlLogLevel: Logger.Level? ) -> Self { - return .init { + .init { FluentSQLiteConfiguration( configuration: configuration, - maxConnectionsPerEventLoop: maxConnectionsPerEventLoop, middleware: [], - connectionPoolTimeout: connectionPoolTimeout + connectionPoolTimeout: connectionPoolTimeout, + dataEncoder: dataEncoder, + dataDecoder: dataDecoder, + sqlLogLevel: sqlLogLevel ) } } @@ -22,22 +86,29 @@ extension DatabaseConfigurationFactory { struct FluentSQLiteConfiguration: DatabaseConfiguration { let configuration: SQLiteConfiguration - let maxConnectionsPerEventLoop: Int - var middleware: [AnyModelMiddleware] + var middleware: [any AnyModelMiddleware] let connectionPoolTimeout: NIO.TimeAmount + let dataEncoder: SQLiteDataEncoder + let dataDecoder: SQLiteDataDecoder + let sqlLogLevel: Logger.Level? - func makeDriver(for databases: Databases) -> DatabaseDriver { + func makeDriver(for databases: Databases) -> any DatabaseDriver { let db = SQLiteConnectionSource( - configuration: configuration, + configuration: self.configuration, threadPool: databases.threadPool ) let pool = EventLoopGroupConnectionPool( source: db, - maxConnectionsPerEventLoop: maxConnectionsPerEventLoop, - requestTimeout: connectionPoolTimeout, + maxConnectionsPerEventLoop: 1, + requestTimeout: self.connectionPoolTimeout, on: databases.eventLoopGroup ) - return _FluentSQLiteDriver(pool: pool) + return FluentSQLiteDriver( + pool: pool, + dataEncoder: self.dataEncoder, + dataDecoder: self.dataDecoder, + sqlLogLevel: self.sqlLogLevel + ) } } diff --git a/Sources/FluentSQLiteDriver/FluentSQLiteDatabase.swift b/Sources/FluentSQLiteDriver/FluentSQLiteDatabase.swift index 491f7ba..5798b86 100644 --- a/Sources/FluentSQLiteDriver/FluentSQLiteDatabase.swift +++ b/Sources/FluentSQLiteDriver/FluentSQLiteDatabase.swift @@ -5,218 +5,153 @@ import SQLiteNIO import SQLKit import FluentKit -struct _FluentSQLiteDatabase { - let database: SQLiteDatabase +struct FluentSQLiteDatabase: Database, SQLDatabase, SQLiteDatabase { + let database: any SQLiteDatabase let context: DatabaseContext + let dataEncoder: SQLiteDataEncoder + let dataDecoder: SQLiteDataDecoder + let queryLogLevel: Logger.Level? let inTransaction: Bool -} - -extension _FluentSQLiteDatabase: Database { - func execute( - query: DatabaseQuery, - onOutput: @escaping (DatabaseOutput) -> () - ) -> EventLoopFuture { - let sql = SQLQueryConverter(delegate: SQLiteConverterDelegate()).convert(query) - let (string, binds) = self.serialize(sql) - let data: [SQLiteData] - do { - data = try binds.map { encodable in - try SQLiteDataEncoder().encode(encodable) - } - } catch { - return self.eventLoop.makeFailedFuture(error) - } - return self.database.withConnection { connection in - connection.logging(to: self.logger) - .query(string, data) { row in - onOutput(row) - } - .flatMap { - switch query.action { - case .create where query.customIDKey != .string(""): - return connection.lastAutoincrementID().map { - let row = LastInsertRow( - lastAutoincrementID: $0, - customIDKey: query.customIDKey - ) - onOutput(row) - } - default: - return self.eventLoop.makeSucceededFuture(()) - } - } + + private func adjustFluentQuery(_ original: DatabaseQuery, _ converted: any SQLExpression) -> any SQLExpression { + /// For `.create` query actions, we want to return the generated IDs, unless the `customIDKey` is the + /// empty string, which we use as a very hacky signal for "we don't implement this for composite IDs yet". + if case .create = original.action, original.customIDKey != .some(.string("")) { + return SQLKit.SQLList([converted, SQLReturning(.init((original.customIDKey ?? .id).description))], separator: SQLRaw(" ")) + } else { + return converted } } - - func transaction(_ closure: @escaping (Database) -> EventLoopFuture) -> EventLoopFuture { - guard !self.inTransaction else { - return closure(self) - } - return self.database.withConnection { conn in - conn.query("BEGIN TRANSACTION").flatMap { _ in - let db = _FluentSQLiteDatabase( - database: conn, - context: self.context, - inTransaction: true - ) - return closure(db).flatMap { result in - conn.query("COMMIT TRANSACTION").map { _ in - result - } - }.flatMapError { error in - conn.query("ROLLBACK TRANSACTION").flatMapThrowing { _ in - throw error - } - } - } - } + + // Database + + func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) -> EventLoopFuture { + /// SQLiteKit will handle applying the configured data decoder to each row when providing `SQLRow`s. + return self.execute( + sql: self.adjustFluentQuery(query, SQLQueryConverter(delegate: SQLiteConverterDelegate()).convert(query)), + { onOutput($0.databaseOutput()) } + ) } + func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> ()) async throws { + try await self.execute( + sql: self.adjustFluentQuery(query, SQLQueryConverter(delegate: SQLiteConverterDelegate()).convert(query)), + { onOutput($0.databaseOutput()) } + ) + } + func execute(schema: DatabaseSchema) -> EventLoopFuture { var schema = schema - switch schema.action { - case .update: - // Remove enum updates as they are unnecessary. - schema.updateFields = schema.updateFields.filter({ - switch $0 { - case .custom: - return true - case .dataType(_, let dataType): - switch dataType { - case .enum: - return false - default: - return true - } - } - }) - guard - schema.createConstraints.isEmpty, - schema.updateFields.isEmpty, - schema.deleteFields.isEmpty, - schema.deleteConstraints.isEmpty + + if schema.action == .update { + schema.updateFields = schema.updateFields.filter { switch $0 { // Filter out enum updates. + case .dataType(_, .enum(_)): return false + default: return true + } } + guard schema.createConstraints.isEmpty, schema.updateFields.isEmpty, + schema.deleteFields.isEmpty, schema.deleteConstraints.isEmpty else { - return self.eventLoop.makeFailedFuture(FluentSQLiteError.unsupportedAlter) + return self.eventLoop.makeFailedFuture(FluentSQLiteUnsupportedAlter()) } - - // If only enum updates, then skip. - if schema.createFields.isEmpty { + if schema.createFields.isEmpty { // If there were only enum updates, bail out. return self.eventLoop.makeSucceededFuture(()) } - default: - break - } - let sql = SQLSchemaConverter(delegate: SQLiteConverterDelegate()).convert(schema) - let (string, binds) = self.serialize(sql) - let data: [SQLiteData] - do { - data = try binds.map { encodable in - try SQLiteDataEncoder().encode(encodable) - } - } catch { - return self.eventLoop.makeFailedFuture(error) - } - return self.database.logging(to: self.logger).query(string, data) { - fatalError("Unexpected output: \($0)") } + + return self.execute( + sql: SQLSchemaConverter(delegate: SQLiteConverterDelegate()).convert(schema), + { self.logger.debug("Unexpected row returned from schema query: \($0)") } + ) } func execute(enum: DatabaseEnum) -> EventLoopFuture { - return self.eventLoop.makeSucceededFuture(()) + self.eventLoop.makeSucceededFuture(()) } - func withConnection(_ closure: @escaping (Database) -> EventLoopFuture) -> EventLoopFuture { - self.database.withConnection { - closure(_FluentSQLiteDatabase(database: $0, context: self.context, inTransaction: self.inTransaction)) - } + func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { + self.eventLoop.makeFutureWithTask { try await self.withConnection { try await closure($0).get() } } } -} - -private enum FluentSQLiteError: Error, CustomStringConvertible { - case unsupportedAlter - - var description: String { - switch self { - case .unsupportedAlter: - return "SQLite only supports adding columns in ALTER TABLE statements." + + func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { + try await self.withConnection { + try await closure(FluentSQLiteDatabase( + database: $0, + context: self.context, + dataEncoder: self.dataEncoder, + dataDecoder: self.dataDecoder, + queryLogLevel: self.queryLogLevel, + inTransaction: self.inTransaction + )) } } -} - -extension _FluentSQLiteDatabase: SQLDatabase { - var dialect: SQLDialect { - SQLiteDialect() + + func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { + self.inTransaction ? + closure(self) : + self.eventLoop.makeFutureWithTask { try await self.transaction { try await closure($0).get() } } } - func execute( - sql query: SQLExpression, - _ onRow: @escaping (SQLRow) -> () - ) -> EventLoopFuture { - self.logging(to: self.logger).sql().execute(sql: query, onRow) + func transaction(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { + guard !self.inTransaction else { + return try await closure(self) + } + + return try await self.withConnection { conn in + _ = try await conn.query("BEGIN TRANSACTION") + do { + let result = try await closure(FluentSQLiteDatabase( + database: conn, + context: self.context, + dataEncoder: self.dataEncoder, + dataDecoder: self.dataDecoder, + queryLogLevel: self.queryLogLevel, + inTransaction: true + )) + + _ = try await conn.query("COMMIT TRANSACTION") + return result + } catch { + _ = try? await conn.query("ROLLBACK TRANSACTION") + throw error + } + } } -} + + // SQLDatabase -extension _FluentSQLiteDatabase: SQLiteDatabase { - func withConnection(_ closure: @escaping (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture { - self.database.withConnection(closure) + var dialect: any SQLDialect { + self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).dialect } - func query( - _ query: String, - _ binds: [SQLiteData], - logger: Logger, - _ onRow: @escaping (SQLiteRow) -> Void - ) -> EventLoopFuture { - self.database.query(query, binds, logger: logger, onRow) + var version: (any SQLDatabaseReportedVersion)? { + self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).version } -} - -private struct LastInsertRow: DatabaseOutput { - var description: String { - ["id": self.lastAutoincrementID].description + + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture { + self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).execute(sql: query, onRow) } - - let lastAutoincrementID: Int - let customIDKey: FieldKey? - - func schema(_ schema: String) -> DatabaseOutput { - return self + + func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) async throws { + try await self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).execute(sql: query, onRow) } - - func contains(_ key: FieldKey) -> Bool { - key == .id || key == self.customIDKey + + func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { + try await self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).withSession(closure) } - func decodeNil(_ key: FieldKey) throws -> Bool { - guard key == .id || key == self.customIDKey else { - fatalError("Cannot decode field from last insert row: \(key).") - } - return false + // SQLiteDatabase + + func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture { + self.withConnection { $0.query(query, binds, logger: logger, onRow) } } - func decode(_ key: FieldKey, as type: T.Type) throws -> T where T : Decodable { - guard key == .id || key == self.customIDKey else { - fatalError("Cannot decode field from last insert row: \(key).") - } - if let autoincrementInitializable = T.self as? AutoincrementIDInitializable.Type { - return autoincrementInitializable.init(autoincrementID: self.lastAutoincrementID) as! T - } else { - fatalError("Unsupported database generated identifier type: \(T.self)") - } + func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture { + self.database.withConnection(closure) } } -protocol AutoincrementIDInitializable { - init(autoincrementID: Int) -} - -extension AutoincrementIDInitializable where Self: FixedWidthInteger { - init(autoincrementID: Int) { - self = numericCast(autoincrementID) +private struct FluentSQLiteUnsupportedAlter: Error, CustomStringConvertible { + var description: String { + "SQLite only supports adding columns in ALTER TABLE statements." } } - -extension Int: AutoincrementIDInitializable { } -extension UInt: AutoincrementIDInitializable { } -extension Int64: AutoincrementIDInitializable { } -extension UInt64: AutoincrementIDInitializable { } diff --git a/Sources/FluentSQLiteDriver/FluentSQLiteDriver.swift b/Sources/FluentSQLiteDriver/FluentSQLiteDriver.swift index 73fac05..30dad95 100644 --- a/Sources/FluentSQLiteDriver/FluentSQLiteDriver.swift +++ b/Sources/FluentSQLiteDriver/FluentSQLiteDriver.swift @@ -1,21 +1,27 @@ import NIOCore import FluentKit -import AsyncKit +@preconcurrency import AsyncKit import SQLiteNIO import SQLiteKit import Logging -struct _FluentSQLiteDriver: DatabaseDriver { +struct FluentSQLiteDriver: DatabaseDriver { let pool: EventLoopGroupConnectionPool + let dataEncoder: SQLiteDataEncoder + let dataDecoder: SQLiteDataDecoder + let sqlLogLevel: Logger.Level? - var eventLoopGroup: EventLoopGroup { + var eventLoopGroup: any EventLoopGroup { self.pool.eventLoopGroup } - func makeDatabase(with context: DatabaseContext) -> Database { - _FluentSQLiteDatabase( - database: _ConnectionPoolSQLiteDatabase(pool: self.pool.pool(for: context.eventLoop), logger: context.logger), + func makeDatabase(with context: DatabaseContext) -> any Database { + FluentSQLiteDatabase( + database: ConnectionPoolSQLiteDatabase(pool: self.pool.pool(for: context.eventLoop), logger: context.logger), context: context, + dataEncoder: self.dataEncoder, + dataDecoder: self.dataDecoder, + queryLogLevel: self.sqlLogLevel, inTransaction: false ) } @@ -25,29 +31,23 @@ struct _FluentSQLiteDriver: DatabaseDriver { } } -struct _ConnectionPoolSQLiteDatabase { +struct ConnectionPoolSQLiteDatabase: SQLiteDatabase { let pool: EventLoopConnectionPool let logger: Logger -} -extension _ConnectionPoolSQLiteDatabase: SQLiteDatabase { - var eventLoop: EventLoop { + var eventLoop: any EventLoop { self.pool.eventLoop } func lastAutoincrementID() -> EventLoopFuture { - self.pool.withConnection(logger: self.logger) { - $0.lastAutoincrementID() - } + self.pool.withConnection(logger: self.logger) { $0.lastAutoincrementID() } } func withConnection(_ closure: @escaping (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture { - self.pool.withConnection { - closure($0) - } + self.pool.withConnection(logger: self.logger) { closure($0) } } - func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping (SQLiteRow) -> Void) -> EventLoopFuture { + func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture { self.withConnection { $0.query(query, binds, logger: logger, onRow) } diff --git a/Sources/FluentSQLiteDriver/SQLiteConverterDelegate.swift b/Sources/FluentSQLiteDriver/SQLiteConverterDelegate.swift index 6dbc022..b1752ed 100644 --- a/Sources/FluentSQLiteDriver/SQLiteConverterDelegate.swift +++ b/Sources/FluentSQLiteDriver/SQLiteConverterDelegate.swift @@ -3,7 +3,7 @@ import SQLKit import FluentKit struct SQLiteConverterDelegate: SQLConverterDelegate { - func customDataType(_ dataType: DatabaseSchema.DataType) -> SQLExpression? { + func customDataType(_ dataType: DatabaseSchema.DataType) -> (any SQLExpression)? { switch dataType { case .string: return SQLRaw("TEXT") @@ -13,7 +13,8 @@ struct SQLiteConverterDelegate: SQLConverterDelegate { return SQLRaw("INTEGER") case .enum: return SQLRaw("TEXT") - default: return nil + default: + return nil } } } diff --git a/Sources/FluentSQLiteDriver/SQLiteError+Database.swift b/Sources/FluentSQLiteDriver/SQLiteError+Database.swift index f0ff8cf..e810b8e 100644 --- a/Sources/FluentSQLiteDriver/SQLiteError+Database.swift +++ b/Sources/FluentSQLiteDriver/SQLiteError+Database.swift @@ -4,7 +4,7 @@ import FluentKit extension SQLiteError: DatabaseError { public var isSyntaxError: Bool { switch self.reason { - case .error: + case .error, .schema: return true default: return false @@ -13,7 +13,7 @@ extension SQLiteError: DatabaseError { public var isConnectionClosed: Bool { switch self.reason { - case .close, .misuse: + case .misuse, .cantOpen: return true default: return false @@ -22,7 +22,10 @@ extension SQLiteError: DatabaseError { public var isConstraintFailure: Bool { switch self.reason { - case .constraint: + case .constraint, .constraintCheckFailed, .constraintUniqueFailed, .constraintTriggerFailed, + .constraintNotNullFailed, .constraintCommitHookFailed, .constraintForeignKeyFailed, + .constraintPrimaryKeyFailed, .constraintUserFunctionFailed, .constraintVirtualTableFailed, + .constraintUniqueRowIDFailed, .constraintStrictDataTypeFailed, .constraintUpdateTriggerDeletedRow: return true default: return false diff --git a/Sources/FluentSQLiteDriver/SQLiteRow+Database.swift b/Sources/FluentSQLiteDriver/SQLiteRow+Database.swift index dd29865..cfe40df 100644 --- a/Sources/FluentSQLiteDriver/SQLiteRow+Database.swift +++ b/Sources/FluentSQLiteDriver/SQLiteRow+Database.swift @@ -1,67 +1,79 @@ import Foundation import SQLiteNIO +import SQLiteKit import FluentKit -extension SQLiteRow: DatabaseOutput { - public func schema(_ schema: String) -> DatabaseOutput { - SchemaOutput(row: self, schema: schema) - } - - public func contains(_ path: FieldKey) -> Bool { - self.column(self.columnName(path)) != nil - } - - public func decodeNil(_ key: FieldKey) throws -> Bool { - try self.decodeNil(column: self.columnName(key)) - } - - public func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T: Decodable - { - try self.decode(column: self.columnName(key), as: T.self) - } - - func columnName(_ key: FieldKey) -> String { - switch key { - case .id: - return "id" - case .aggregate: - return key.description - case .string(let name): - return name - case .prefix(let prefix, let key): - return self.columnName(prefix) + self.columnName(key) - } +extension SQLRow { + /// Returns a `DatabaseOutput` for this row. + /// + /// - Returns: A `DatabaseOutput` instance. + func databaseOutput() -> some DatabaseOutput { + SQLRowDatabaseOutput(row: self, schema: nil) } } -private struct SchemaOutput: DatabaseOutput { - let row: SQLiteRow - let schema: String +/// A `DatabaseOutput` implementation for generic `SQLRow`s. This should really be in FluentSQL. +private struct SQLRowDatabaseOutput: DatabaseOutput { + /// The underlying row. + let row: any SQLRow + /// The most recently set schema value (see `DatabaseOutput.schema(_:)`). + let schema: String? + + // See `CustomStringConvertible.description`. var description: String { - self.row.description + String(describing: self.row) } - func schema(_ schema: String) -> DatabaseOutput { - SchemaOutput(row: self.row, schema: schema) + /// Apply the current schema (if any) to the given `FieldKey` and convert to a column name. + private func adjust(key: FieldKey) -> String { + (self.schema.map { .prefix(.prefix(.string($0), "_"), key) } ?? key).description } - + + // See `DatabaseOutput.schema(_:)`. + func schema(_ schema: String) -> any DatabaseOutput { + SQLRowDatabaseOutput(row: self.row, schema: schema) + } + + // See `DatabaseOutput.contains(_:)`. func contains(_ key: FieldKey) -> Bool { - self.row.contains(self.key(key)) + self.row.contains(column: self.adjust(key: key)) } - + + // See `DatabaseOutput.decodeNil(_:)`. func decodeNil(_ key: FieldKey) throws -> Bool { - try self.row.decodeNil(self.key(key)) + try self.row.decodeNil(column: self.adjust(key: key)) } - - func decode(_ key: FieldKey, as type: T.Type) throws -> T - where T : Decodable - { - try self.row.decode(self.key(key), as: T.self) + + // See `DatabaseOutput.decode(_:as:)`. + func decode(_ key: FieldKey, as: T.Type) throws -> T { + try self.row.decode(column: self.adjust(key: key), as: T.self) } +} - func key(_ key: FieldKey) -> FieldKey { - .prefix(.string(self.schema + "_"), key) +/// A legacy deprecated conformance of `SQLiteRow` directly to `DatabaseOutput`. This interface exists solely +/// because its absence would be a public API break. +/// +/// Do not use these methods. +@available(*, deprecated, message: "Do not use this conformance.") +extension SQLiteNIO.SQLiteRow: FluentKit.DatabaseOutput { + // See `DatabaseOutput.schema(_:)`. + public func schema(_ schema: String) -> any DatabaseOutput { + self.databaseOutput().schema(schema) + } + + // See `DatabaseOutput.contains(_:)`. + public func contains(_ key: FieldKey) -> Bool { + self.databaseOutput().contains(key) + } + + // See `DatabaseOutput.decodeNil(_:)`. + public func decodeNil(_ key: FieldKey) throws -> Bool { + try self.databaseOutput().decodeNil(key) + } + + // See `DatabaseOutput.decode(_:as:)`. + public func decode(_ key: FieldKey, as: T.Type) throws -> T { + try self.databaseOutput().decode(key, as: T.self) } } diff --git a/Tests/FluentSQLiteDriverTests/FluentSQLiteDriverTests.swift b/Tests/FluentSQLiteDriverTests/FluentSQLiteDriverTests.swift index ad7f2f7..cefe34d 100644 --- a/Tests/FluentSQLiteDriverTests/FluentSQLiteDriverTests.swift +++ b/Tests/FluentSQLiteDriverTests/FluentSQLiteDriverTests.swift @@ -8,11 +8,37 @@ import SQLiteNIO import FluentKit import SQLKit +func XCTAssertThrowsErrorAsync( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, line: UInt = #line, + _ callback: (any Error) -> Void = { _ in } +) async { + do { + _ = try await expression() + XCTAssertThrowsError({}(), message(), file: file, line: line, callback) + } catch { + XCTAssertThrowsError(try { throw error }(), message(), file: file, line: line, callback) + } +} + +func XCTAssertNoThrowAsync( + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, line: UInt = #line +) async { + do { + _ = try await expression() + } catch { + XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line) + } +} + final class FluentSQLiteDriverTests: XCTestCase { - //func testAll() throws { try self.benchmarker.testAll() } func testAggregate() throws { try self.benchmarker.testAggregate() } func testArray() throws { try self.benchmarker.testArray() } func testBatch() throws { try self.benchmarker.testBatch() } + func testChild() throws { try self.benchmarker.testChild() } func testChildren() throws { try self.benchmarker.testChildren() } func testCodable() throws { try self.benchmarker.testCodable() } func testChunk() throws { try self.benchmarker.testChunk() } @@ -42,42 +68,30 @@ final class FluentSQLiteDriverTests: XCTestCase { func testTransaction() throws { try self.benchmarker.testTransaction() } func testUnique() throws { try self.benchmarker.testUnique() } - func testDatabaseError() throws { - let sql = (self.database as! SQLDatabase) - do { - try sql.raw("asdf").run().wait() - } catch let error as DatabaseError where error.isSyntaxError { - // pass - } catch { - XCTFail("\(error)") + func testDatabaseError() async throws { + let sql = (self.database as! any SQLDatabase) + + await XCTAssertThrowsErrorAsync(try await sql.raw("asdf").run()) { + XCTAssertTrue(($0 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))") } - do { - try sql.raw("CREATE TABLE foo (name TEXT UNIQUE)").run().wait() - try sql.raw("INSERT INTO foo (name) VALUES ('bar')").run().wait() - try sql.raw("INSERT INTO foo (name) VALUES ('bar')").run().wait() - } catch let error as DatabaseError where error.isConstraintFailure { - // pass - } catch { - XCTFail("\(error)") - } - do { - try (sql as! SQLiteDatabase).withConnection { conn in - conn.close().flatMap { - conn.sql().raw("INSERT INTO foo (name) VALUES ('bar')").run() - } - }.wait() - } catch let error as DatabaseError where error.isConnectionClosed { - // pass - } catch { - XCTFail("\(error)") + + try await sql.drop(table: "foo").ifExists().run() + try await sql.create(table: "foo").column("name", type: .text, .unique).run() + try await sql.insert(into: "foo").columns("name").values("bar").run() + await XCTAssertThrowsErrorAsync(try await sql.insert(into: "foo").columns("name").values("bar").run()) { + XCTAssertTrue(($0 as? any DatabaseError)?.isConstraintFailure ?? false, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))") + XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))") } } // https://github.com/vapor/fluent-sqlite-driver/issues/62 - func testUnsupportedUpdateMigration() throws { - struct UserMigration_v1_0_0: Migration { - func prepare(on database: Database) -> EventLoopFuture { - database.schema("users") + func testUnsupportedUpdateMigration() async throws { + struct UserMigration_v1_0_0: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema("users") .id() .field("email", .string, .required) .field("password", .string, .required) @@ -85,86 +99,117 @@ final class FluentSQLiteDriverTests: XCTestCase { .create() } - func revert(on database: Database) -> EventLoopFuture { - database.schema("users").delete() + func revert(on database: any Database) async throws { + try await database.schema("users").delete() } } - struct UserMigration_v1_2_0: Migration { - func prepare(on database: Database) -> EventLoopFuture { - database.schema("users") + + struct UserMigration_v1_2_0: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema("users") .field("apple_id", .string) .unique(on: "apple_id") .update() } - func revert(on database: Database) -> EventLoopFuture { - database.schema("users") + func revert(on database: any Database) async throws { + try await database.schema("users") .deleteField("apple_id") .update() } } - try UserMigration_v1_0_0().prepare(on: self.database).wait() + + try await UserMigration_v1_0_0().prepare(on: self.database) + await XCTAssertThrowsErrorAsync(try await UserMigration_v1_2_0().prepare(on: self.database)) { + XCTAssert(String(describing: $0).contains("adding columns")) + } + await XCTAssertThrowsErrorAsync(try await UserMigration_v1_2_0().revert(on: self.database)) { + XCTAssert(String(describing: $0).contains("adding columns")) + } + await XCTAssertNoThrowAsync(try await UserMigration_v1_0_0().revert(on: self.database)) + } + + func testCustomJSON() async throws { + struct Metadata: Codable { let createdAt: Date } + final class Event: Model, @unchecked Sendable { + static let schema = "events" + @ID(custom: "id", generatedBy: .database) var id: Int? + @Field(key: "metadata") var metadata: Metadata + } + final class EventStringlyTyped: Model, @unchecked Sendable { + static let schema = "events" + @ID(custom: "id", generatedBy: .database) var id: Int? + @Field(key: "metadata") var metadata: [String: String] + } + struct EventMigration: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema(Event.schema) + .field("id", .int, .identifier(auto: false)) + .field("metadata", .json, .required) + .create() + } + func revert(on database: any Database) async throws { + try await database.schema(Event.schema).delete() + } + } + + let jsonEncoder = JSONEncoder(); jsonEncoder.dateEncodingStrategy = .iso8601 + let jsonDecoder = JSONDecoder(); jsonDecoder.dateDecodingStrategy = .iso8601 + let iso8601 = DatabaseID(string: "iso8601") + + self.dbs.use(.sqlite(.memory, dataEncoder: .init(json: jsonEncoder), dataDecoder: .init(json: jsonDecoder)), as: iso8601) + let db = self.dbs.database(iso8601, logger: .init(label: "test"), on: self.dbs.eventLoopGroup.any())! + + try await EventMigration().prepare(on: db) do { - try UserMigration_v1_2_0().prepare(on: self.database).wait() - try UserMigration_v1_2_0().revert(on: self.database).wait() + let date = Date() + let event = Event() + event.id = 1 + event.metadata = Metadata(createdAt: date) + try await event.save(on: db) + + let rows = try await EventStringlyTyped.query(on: db).filter(\.$id == 1).all() + XCTAssertEqual(rows[0].metadata["createdAt"], ISO8601DateFormatter().string(from: date)) } catch { - print(error) - XCTAssertTrue("\(error)".contains("adding columns")) + try? await EventMigration().revert(on: db) + throw error } - try UserMigration_v1_0_0().revert(on: self.database).wait() - } - - var benchmarker: FluentBenchmarker { - return .init(databases: self.dbs) + try await EventMigration().revert(on: db) } - - var database: Database { - self.benchmarker.database - } - - var threadPool: NIOThreadPool! - var eventLoopGroup: EventLoopGroup! - var dbs: Databases! + var benchmarker: FluentBenchmarker { .init(databases: self.dbs) } + var database: (any Database)! + var dbs: Databases! let benchmarkPath = FileManager.default.temporaryDirectory.appendingPathComponent("benchmark.sqlite").absoluteString + override class func setUp() { + XCTAssert(isLoggingConfigured) + } + override func setUpWithError() throws { try super.setUpWithError() - - XCTAssert(isLoggingConfigured) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - self.threadPool = .init(numberOfThreads: System.coreCount) - self.threadPool.start() - self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup) + self.dbs = Databases(threadPool: NIOThreadPool.singleton, on: MultiThreadedEventLoopGroup.singleton) self.dbs.use(.sqlite(.memory), as: .sqlite) - self.dbs.use(.sqlite(.file(self.benchmarkPath)), as: .benchmark) + self.dbs.use(.sqlite(.file(self.benchmarkPath)), as: .init(string: "benchmark")) + self.database = self.dbs.database(.sqlite, logger: .init(label: "test.fluent.sqlite"), on: MultiThreadedEventLoopGroup.singleton.any()) } override func tearDownWithError() throws { self.dbs.shutdown() self.dbs = nil - try self.threadPool.syncShutdownGracefully() - self.threadPool = nil - try self.eventLoopGroup.syncShutdownGracefully() - self.eventLoopGroup = nil - try super.tearDownWithError() } } func env(_ name: String) -> String? { - return ProcessInfo.processInfo.environment[name] + ProcessInfo.processInfo.environment[name] } let isLoggingConfigured: Bool = { LoggingSystem.bootstrap { label in var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .debug + handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .debug return handler } return true }() - -extension DatabaseID { - static let benchmark = DatabaseID(string: "benchmark") -}