├── .editorconfig ├── .gitignore ├── .github ├── dependabot.yml ├── CODEOWNERS └── workflows │ ├── api-docs.yml │ └── test.yml ├── .spi.yml ├── Sources └── FluentSQLiteDriver │ ├── Exports.swift │ ├── SQLiteConverterDelegate.swift │ ├── Docs.docc │ ├── index.md │ ├── theme-settings.json │ └── Resources │ │ └── vapor-fluentsqlitedriver-logo.svg │ ├── SQLiteError+Database.swift │ ├── FluentSQLiteDriver.swift │ ├── SQLiteRow+Database.swift │ ├── FluentSQLiteConfiguration.swift │ └── FluentSQLiteDatabase.swift ├── LICENSE ├── Package.swift ├── README.md ├── .swift-format └── Tests └── FluentSQLiteDriverTests └── FluentSQLiteDriverTests.swift /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.swift] 2 | indent_style = space 3 | indent_size = 4 4 | tab_width = 4 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Sources/main.swift 3 | .build 4 | Packages 5 | Database 6 | *.xcodeproj 7 | database.db 8 | Package.resolved 9 | DerivedData 10 | .swiftpm 11 | Tests/LinuxMain.swift 12 | .vscode 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | groups: 8 | dependencies: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | metadata: 3 | authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community." 4 | external_links: 5 | documentation: "https://api.vapor.codes/fluentsqlitedriver/documentation/fluentsqlitedriver/" 6 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/Exports.swift: -------------------------------------------------------------------------------- 1 | @_documentation(visibility: internal) @_exported import FluentKit 2 | @_documentation(visibility: internal) @_exported import SQLiteKit 3 | 4 | extension DatabaseID { 5 | public static var sqlite: DatabaseID { 6 | .init(string: "sqlite") 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @gwynne 2 | /.github/CONTRIBUTING.md @gwynne @0xTim 3 | /.github/workflows/*.yml @gwynne @0xTim 4 | /.github/workflows/test.yml @gwynne 5 | /.spi.yml @gwynne @0xTim 6 | /.gitignore @gwynne @0xTim 7 | /LICENSE @gwynne @0xTim 8 | /README.md @gwynne @0xTim 9 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy-api-docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main 10 | secrets: inherit 11 | with: 12 | package_name: fluent-sqlite-driver 13 | modules: FluentSQLiteDriver 14 | pathsToInvalidate: /fluentsqlitedriver/* 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | pull_request: { types: [opened, reopened, synchronize, ready_for_review] } 7 | push: { branches: [ main ] } 8 | 9 | jobs: 10 | unit-tests: 11 | uses: vapor/ci/.github/workflows/run-unit-tests.yml@main 12 | secrets: inherit 13 | with: 14 | with_android: true 15 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/SQLiteConverterDelegate.swift: -------------------------------------------------------------------------------- 1 | import FluentKit 2 | import FluentSQL 3 | import SQLKit 4 | 5 | struct SQLiteConverterDelegate: SQLConverterDelegate { 6 | func customDataType(_ dataType: DatabaseSchema.DataType) -> (any SQLExpression)? { 7 | switch dataType { 8 | case .string: 9 | SQLRaw("TEXT") 10 | case .datetime: 11 | SQLRaw("REAL") 12 | case .int64: 13 | SQLRaw("INTEGER") 14 | case .enum: 15 | SQLRaw("TEXT") 16 | default: 17 | nil 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``FluentSQLiteDriver`` 2 | 3 | FluentSQLiteDriver is a [FluentKit] driver for SQLite clients. 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | [FluentKit]: https://github.com/vapor/fluent-kit 10 | [SQLKit]: https://github.com/vapor/sql-kit 11 | [SQLiteKit]: https://github.com/vapor/sqlite-kit 12 | [SQLiteNIO]: https://github.com/vapor/sqlite-nio 13 | [AsyncKit]: https://github.com/vapor/async-kit 14 | 15 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/Docs.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, 4 | "border-radius": "0", 5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 7 | "color": { 8 | "fluentsqlitedriver": "hsl(215, 45%, 58%)", 9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-fluentsqlitedriver) 30%, #000 100%)", 10 | "documentation-intro-accent": "var(--color-fluentsqlitedriver)", 11 | "documentation-intro-eyebrow": "white", 12 | "documentation-intro-figure": "white", 13 | "documentation-intro-title": "white", 14 | "logo-base": { "dark": "#fff", "light": "#000" }, 15 | "logo-shape": { "dark": "#000", "light": "#fff" }, 16 | "fill": { "dark": "#000", "light": "#fff" } 17 | }, 18 | "icons": { "technology": "/fluentsqlitedriver/images/FluentSQLiteDriver/vapor-fluentsqlitedriver-logo.svg" } 19 | }, 20 | "features": { 21 | "quickNavigation": { "enable": true }, 22 | "i18n": { "enable": true } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/SQLiteError+Database.swift: -------------------------------------------------------------------------------- 1 | import FluentKit 2 | import SQLiteNIO 3 | 4 | // Required for Database Error 5 | extension SQLiteError { 6 | public var isSyntaxError: Bool { 7 | switch self.reason { 8 | case .error, .schema: 9 | true 10 | default: 11 | false 12 | } 13 | } 14 | 15 | public var isConnectionClosed: Bool { 16 | switch self.reason { 17 | case .misuse, .cantOpen: 18 | true 19 | default: 20 | false 21 | } 22 | } 23 | 24 | public var isConstraintFailure: Bool { 25 | switch self.reason { 26 | case .constraint, .constraintCheckFailed, .constraintUniqueFailed, .constraintTriggerFailed, 27 | .constraintNotNullFailed, .constraintCommitHookFailed, .constraintForeignKeyFailed, 28 | .constraintPrimaryKeyFailed, .constraintUserFunctionFailed, .constraintVirtualTableFailed, 29 | .constraintUniqueRowIDFailed, .constraintStrictDataTypeFailed, .constraintUpdateTriggerDeletedRow: 30 | true 31 | default: 32 | false 33 | } 34 | } 35 | } 36 | 37 | #if compiler(<6) 38 | extension SQLiteError: DatabaseError {} 39 | #else 40 | extension SQLiteError: @retroactive DatabaseError {} 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/Docs.docc/Resources/vapor-fluentsqlitedriver-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "fluent-sqlite-driver", 6 | platforms: [ 7 | .macOS(.v10_15), 8 | .iOS(.v13), 9 | .watchOS(.v6), 10 | .tvOS(.v13), 11 | ], 12 | products: [ 13 | .library(name: "FluentSQLiteDriver", targets: ["FluentSQLiteDriver"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.51.0"), 17 | .package(url: "https://github.com/vapor/sqlite-kit.git", from: "4.5.2"), 18 | .package(url: "https://github.com/apple/swift-log.git", from: "1.6.3"), 19 | ], 20 | targets: [ 21 | .target( 22 | name: "FluentSQLiteDriver", 23 | dependencies: [ 24 | .product(name: "FluentKit", package: "fluent-kit"), 25 | .product(name: "FluentSQL", package: "fluent-kit"), 26 | .product(name: "Logging", package: "swift-log"), 27 | .product(name: "SQLiteKit", package: "sqlite-kit"), 28 | ], 29 | swiftSettings: swiftSettings 30 | ), 31 | .testTarget( 32 | name: "FluentSQLiteDriverTests", 33 | dependencies: [ 34 | .product(name: "FluentBenchmark", package: "fluent-kit"), 35 | .target(name: "FluentSQLiteDriver"), 36 | ], 37 | swiftSettings: swiftSettings 38 | ), 39 | ] 40 | ) 41 | 42 | var swiftSettings: [SwiftSetting] { [ 43 | .enableUpcomingFeature("ExistentialAny"), 44 | .enableUpcomingFeature("ConciseMagicFile"), 45 | .enableUpcomingFeature("ForwardTrailingClosures"), 46 | .enableUpcomingFeature("DisableOutwardActorInference"), 47 | .enableExperimentalFeature("StrictConcurrency=complete"), 48 | ] } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | FluentSQLiteDriver 3 |
4 |
5 | Documentation 6 | Team Chat 7 | MIT License 8 | Continuous Integration 9 | 10 | Swift 5.10+ 11 |

12 | 13 |
14 | 15 | 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. 16 | 17 | [FluentKit]: https://github.com/vapor/fluent-kit 18 | [SQLKit]: https://github.com/vapor/sql-kit 19 | [SQLiteKit]: https://github.com/vapor/sqlite-kit 20 | [SQLiteNIO]: https://github.com/vapor/sqlite-nio 21 | [AsyncKit]: https://github.com/vapor/async-kit 22 | 23 | ### Usage 24 | 25 | Use the SPM string to easily include the dependendency in your `Package.swift` file: 26 | 27 | ```swift 28 | .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0") 29 | ``` 30 | 31 | For additional information, see [the Fluent documentation](https://docs.vapor.codes/fluent/overview/). 32 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/FluentSQLiteDriver.swift: -------------------------------------------------------------------------------- 1 | @preconcurrency import AsyncKit 2 | import FluentKit 3 | import Logging 4 | import NIOCore 5 | import SQLiteKit 6 | import SQLiteNIO 7 | 8 | struct FluentSQLiteDriver: DatabaseDriver { 9 | let pool: EventLoopGroupConnectionPool 10 | let dataEncoder: SQLiteDataEncoder 11 | let dataDecoder: SQLiteDataDecoder 12 | let sqlLogLevel: Logger.Level? 13 | 14 | var eventLoopGroup: any EventLoopGroup { 15 | self.pool.eventLoopGroup 16 | } 17 | 18 | func makeDatabase(with context: DatabaseContext) -> any Database { 19 | FluentSQLiteDatabase( 20 | database: ConnectionPoolSQLiteDatabase(pool: self.pool.pool(for: context.eventLoop), logger: context.logger), 21 | context: context, 22 | dataEncoder: self.dataEncoder, 23 | dataDecoder: self.dataDecoder, 24 | queryLogLevel: self.sqlLogLevel, 25 | inTransaction: false 26 | ) 27 | } 28 | 29 | func shutdown() { 30 | try? self.pool.syncShutdownGracefully() 31 | } 32 | 33 | func shutdownAsync() async { 34 | try? await self.pool.shutdownAsync() 35 | } 36 | } 37 | 38 | struct ConnectionPoolSQLiteDatabase: SQLiteDatabase { 39 | let pool: EventLoopConnectionPool 40 | let logger: Logger 41 | 42 | var eventLoop: any EventLoop { 43 | self.pool.eventLoop 44 | } 45 | 46 | func lastAutoincrementID() -> EventLoopFuture { 47 | self.pool.withConnection(logger: self.logger) { $0.lastAutoincrementID() } 48 | } 49 | 50 | func withConnection(_ closure: @escaping (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture { 51 | self.pool.withConnection(logger: self.logger) { closure($0) } 52 | } 53 | 54 | func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture { 55 | self.withConnection { 56 | $0.query(query, binds, logger: logger, onRow) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/SQLiteRow+Database.swift: -------------------------------------------------------------------------------- 1 | import FluentKit 2 | import Foundation 3 | import SQLiteKit 4 | import SQLiteNIO 5 | 6 | extension SQLRow { 7 | /// Returns a `DatabaseOutput` for this row. 8 | /// 9 | /// - Returns: A `DatabaseOutput` instance. 10 | func databaseOutput() -> some DatabaseOutput { 11 | SQLRowDatabaseOutput(row: self, schema: nil) 12 | } 13 | } 14 | 15 | /// A `DatabaseOutput` implementation for generic `SQLRow`s. This should really be in FluentSQL. 16 | private struct SQLRowDatabaseOutput: DatabaseOutput { 17 | /// The underlying row. 18 | let row: any SQLRow 19 | 20 | /// The most recently set schema value (see `DatabaseOutput.schema(_:)`). 21 | let schema: String? 22 | 23 | // See `CustomStringConvertible.description`. 24 | var description: String { 25 | String(describing: self.row) 26 | } 27 | 28 | /// Apply the current schema (if any) to the given `FieldKey` and convert to a column name. 29 | private func adjust(key: FieldKey) -> String { 30 | (self.schema.map { .prefix(.prefix(.string($0), "_"), key) } ?? key).description 31 | } 32 | 33 | // See `DatabaseOutput.schema(_:)`. 34 | func schema(_ schema: String) -> any DatabaseOutput { 35 | SQLRowDatabaseOutput(row: self.row, schema: schema) 36 | } 37 | 38 | // See `DatabaseOutput.contains(_:)`. 39 | func contains(_ key: FieldKey) -> Bool { 40 | self.row.contains(column: self.adjust(key: key)) 41 | } 42 | 43 | // See `DatabaseOutput.decodeNil(_:)`. 44 | func decodeNil(_ key: FieldKey) throws -> Bool { 45 | try self.row.decodeNil(column: self.adjust(key: key)) 46 | } 47 | 48 | // See `DatabaseOutput.decode(_:as:)`. 49 | func decode(_ key: FieldKey, as: T.Type) throws -> T { 50 | try self.row.decode(column: self.adjust(key: key), as: T.self) 51 | } 52 | } 53 | 54 | /// A legacy deprecated conformance of `SQLiteRow` directly to `DatabaseOutput`. This interface exists solely 55 | /// because its absence would be a public API break. 56 | /// 57 | /// Do not use these methods. 58 | @available(*, deprecated, message: "Do not use this conformance.") 59 | extension SQLiteNIO.SQLiteRow: FluentKit.DatabaseOutput { 60 | // See `DatabaseOutput.schema(_:)`. 61 | public func schema(_ schema: String) -> any DatabaseOutput { 62 | self.databaseOutput().schema(schema) 63 | } 64 | 65 | // See `DatabaseOutput.contains(_:)`. 66 | public func contains(_ key: FieldKey) -> Bool { 67 | self.databaseOutput().contains(key) 68 | } 69 | 70 | // See `DatabaseOutput.decodeNil(_:)`. 71 | public func decodeNil(_ key: FieldKey) throws -> Bool { 72 | try self.databaseOutput().decodeNil(key) 73 | } 74 | 75 | // See `DatabaseOutput.decode(_:as:)`. 76 | public func decode(_ key: FieldKey, as: T.Type) throws -> T { 77 | try self.databaseOutput().decode(key, as: T.self) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "fileScopedDeclarationPrivacy" : { 3 | "accessLevel" : "private" 4 | }, 5 | "indentConditionalCompilationBlocks" : false, 6 | "indentSwitchCaseLabels" : false, 7 | "indentation" : { 8 | "spaces" : 4 9 | }, 10 | "lineBreakAroundMultilineExpressionChainComponents" : false, 11 | "lineBreakBeforeControlFlowKeywords" : false, 12 | "lineBreakBeforeEachArgument" : false, 13 | "lineBreakBeforeEachGenericRequirement" : false, 14 | "lineBreakBetweenDeclarationAttributes" : false, 15 | "lineLength" : 150, 16 | "maximumBlankLines" : 1, 17 | "multiElementCollectionTrailingCommas" : true, 18 | "noAssignmentInExpressions" : { 19 | "allowedFunctions" : [ 20 | ] 21 | }, 22 | "prioritizeKeepingFunctionOutputTogether" : false, 23 | "reflowMultilineStringLiterals" : { 24 | "never" : { 25 | } 26 | }, 27 | "respectsExistingLineBreaks" : true, 28 | "rules" : { 29 | "AllPublicDeclarationsHaveDocumentation" : false, 30 | "AlwaysUseLiteralForEmptyCollectionInit" : true, 31 | "AlwaysUseLowerCamelCase" : true, 32 | "AmbiguousTrailingClosureOverload" : true, 33 | "AvoidRetroactiveConformances" : true, 34 | "BeginDocumentationCommentWithOneLineSummary" : false, 35 | "DoNotUseSemicolons" : true, 36 | "DontRepeatTypeInStaticProperties" : true, 37 | "FileScopedDeclarationPrivacy" : true, 38 | "FullyIndirectEnum" : true, 39 | "GroupNumericLiterals" : true, 40 | "IdentifiersMustBeASCII" : true, 41 | "NeverForceUnwrap" : false, 42 | "NeverUseForceTry" : false, 43 | "NeverUseImplicitlyUnwrappedOptionals" : false, 44 | "NoAccessLevelOnExtensionDeclaration" : true, 45 | "NoAssignmentInExpressions" : true, 46 | "NoBlockComments" : true, 47 | "NoCasesWithOnlyFallthrough" : true, 48 | "NoEmptyLinesOpeningClosingBraces" : false, 49 | "NoEmptyTrailingClosureParentheses" : true, 50 | "NoLabelsInCasePatterns" : true, 51 | "NoLeadingUnderscores" : false, 52 | "NoParensAroundConditions" : true, 53 | "NoPlaygroundLiterals" : true, 54 | "NoVoidReturnOnFunctionSignature" : true, 55 | "OmitExplicitReturns" : false, 56 | "OneCasePerLine" : true, 57 | "OneVariableDeclarationPerLine" : true, 58 | "OnlyOneTrailingClosureArgument" : true, 59 | "OrderedImports" : true, 60 | "ReplaceForEachWithForLoop" : true, 61 | "ReturnVoidInsteadOfEmptyTuple" : true, 62 | "TypeNamesShouldBeCapitalized" : true, 63 | "UseEarlyExits" : true, 64 | "UseExplicitNilCheckInConditions" : true, 65 | "UseLetInEveryBoundCaseVariable" : true, 66 | "UseShorthandTypeNames" : true, 67 | "UseSingleLinePropertyGetter" : true, 68 | "UseSynthesizedInitializer" : true, 69 | "UseTripleSlashForDocumentationComments" : true, 70 | "UseWhereClausesInForLoops" : false, 71 | "ValidateDocumentationComments" : false 72 | }, 73 | "spacesAroundRangeFormationOperators" : true, 74 | "spacesBeforeEndOfLineComments" : 1, 75 | "tabWidth" : 4, 76 | "version" : 1 77 | } 78 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/FluentSQLiteConfiguration.swift: -------------------------------------------------------------------------------- 1 | import AsyncKit 2 | import FluentKit 3 | import Logging 4 | import NIO 5 | import SQLiteKit 6 | 7 | // Hint: Yes, I know what default arguments are. This ridiculous spelling out of each alternative avoids public API 8 | // breakage from adding the defaults. And yes, `maxConnectionsPerEventLoop` is not forwarded on purpose, it's not 9 | // an oversight or an omission. We no longer support it for SQLite because increasing it past one causes thread 10 | // conntention but can never increase parallelism. 11 | 12 | extension DatabaseConfigurationFactory { 13 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 14 | public static func sqlite( 15 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10) 16 | ) -> Self { 17 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: .init(), sqlLogLevel: .debug) 18 | } 19 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 20 | public static func sqlite( 21 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), 22 | dataEncoder: SQLiteDataEncoder 23 | ) -> Self { 24 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: dataEncoder, dataDecoder: .init(), sqlLogLevel: .debug) 25 | } 26 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 27 | public static func sqlite( 28 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), 29 | dataDecoder: SQLiteDataDecoder 30 | ) -> Self { 31 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: dataDecoder, sqlLogLevel: .debug) 32 | } 33 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 34 | public static func sqlite( 35 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), 36 | dataEncoder: SQLiteDataEncoder, dataDecoder: SQLiteDataDecoder 37 | ) -> Self { 38 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: dataEncoder, dataDecoder: dataDecoder, sqlLogLevel: .debug) 39 | } 40 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 41 | public static func sqlite( 42 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), 43 | sqlLogLevel: Logger.Level? 44 | ) -> Self { 45 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: .init(), sqlLogLevel: sqlLogLevel) 46 | } 47 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 48 | public static func sqlite( 49 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), 50 | dataEncoder: SQLiteDataEncoder, sqlLogLevel: Logger.Level? 51 | ) -> Self { 52 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: dataEncoder, dataDecoder: .init(), sqlLogLevel: sqlLogLevel) 53 | } 54 | /// Shorthand for ``sqlite(_:maxConnectionsPerEventLoop:connectionPoolTimeout:dataEncoder:dataDecoder:sqlLogLevel:)``. 55 | public static func sqlite( 56 | _ config: SQLiteConfiguration = .memory, maxConnectionsPerEventLoop: Int = 1, connectionPoolTimeout: TimeAmount = .seconds(10), 57 | dataDecoder: SQLiteDataDecoder, sqlLogLevel: Logger.Level? 58 | ) -> Self { 59 | self.sqlite(config, connectionPoolTimeout: connectionPoolTimeout, dataEncoder: .init(), dataDecoder: dataDecoder, sqlLogLevel: sqlLogLevel) 60 | } 61 | 62 | /// Return a configuration factory using the provided parameters. 63 | /// 64 | /// - Parameters: 65 | /// - configuration: The underlying `SQLiteConfiguration`. 66 | /// - maxConnectionsPerEventLoop: Ignored. The value is always treated as 1. 67 | /// - dataEncoder: An `SQLiteDataEncoder` used to translate bound query parameters into `SQLiteData` values. 68 | /// - dataDecoder: An `SQLiteDataDecoder` used to translate `SQLiteData` values into output values. 69 | /// - queryLogLevel: The level at which SQL queries issued through the Fluent or SQLKit interfaces will be logged. 70 | /// - Returns: A configuration factory, 71 | public static func sqlite( 72 | _ configuration: SQLiteConfiguration = .memory, 73 | maxConnectionsPerEventLoop: Int = 1, 74 | connectionPoolTimeout: TimeAmount = .seconds(10), 75 | dataEncoder: SQLiteDataEncoder, 76 | dataDecoder: SQLiteDataDecoder, 77 | sqlLogLevel: Logger.Level? 78 | ) -> Self { 79 | .init { 80 | FluentSQLiteConfiguration( 81 | configuration: configuration, 82 | middleware: [], 83 | connectionPoolTimeout: connectionPoolTimeout, 84 | dataEncoder: dataEncoder, 85 | dataDecoder: dataDecoder, 86 | sqlLogLevel: sqlLogLevel 87 | ) 88 | } 89 | } 90 | } 91 | 92 | struct FluentSQLiteConfiguration: DatabaseConfiguration { 93 | let configuration: SQLiteConfiguration 94 | var middleware: [any AnyModelMiddleware] 95 | let connectionPoolTimeout: NIO.TimeAmount 96 | let dataEncoder: SQLiteDataEncoder 97 | let dataDecoder: SQLiteDataDecoder 98 | let sqlLogLevel: Logger.Level? 99 | 100 | func makeDriver(for databases: Databases) -> any DatabaseDriver { 101 | let db = SQLiteConnectionSource( 102 | configuration: self.configuration, 103 | threadPool: databases.threadPool 104 | ) 105 | let pool = EventLoopGroupConnectionPool( 106 | source: db, 107 | maxConnectionsPerEventLoop: 1, 108 | requestTimeout: self.connectionPoolTimeout, 109 | on: databases.eventLoopGroup 110 | ) 111 | return FluentSQLiteDriver( 112 | pool: pool, 113 | dataEncoder: self.dataEncoder, 114 | dataDecoder: self.dataDecoder, 115 | sqlLogLevel: self.sqlLogLevel 116 | ) 117 | } 118 | } 119 | 120 | extension SQLiteConfiguration { 121 | public static func file(_ path: String) -> Self { 122 | .init(storage: .file(path: path)) 123 | } 124 | 125 | public static var memory: Self { 126 | .init(storage: .memory) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/FluentSQLiteDriver/FluentSQLiteDatabase.swift: -------------------------------------------------------------------------------- 1 | import FluentKit 2 | import FluentSQL 3 | import NIOCore 4 | import SQLKit 5 | import SQLiteKit 6 | import SQLiteNIO 7 | 8 | struct FluentSQLiteDatabase: Database, SQLDatabase, SQLiteDatabase { 9 | let database: any SQLiteDatabase 10 | let context: DatabaseContext 11 | let dataEncoder: SQLiteDataEncoder 12 | let dataDecoder: SQLiteDataDecoder 13 | let queryLogLevel: Logger.Level? 14 | let inTransaction: Bool 15 | 16 | private func adjustFluentQuery(_ original: DatabaseQuery, _ converted: any SQLExpression) -> any SQLExpression { 17 | /// For `.create` query actions, we want to return the generated IDs, unless the `customIDKey` is the 18 | /// empty string, which we use as a very hacky signal for "we don't implement this for composite IDs yet". 19 | guard case .create = original.action, original.customIDKey != .some(.string("")) else { 20 | return converted 21 | } 22 | return SQLKit.SQLList([converted, SQLReturning(.init((original.customIDKey ?? .id).description))], separator: SQLRaw(" ")) 23 | } 24 | 25 | // Database 26 | 27 | func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) -> EventLoopFuture { 28 | /// SQLiteKit will handle applying the configured data decoder to each row when providing `SQLRow`s. 29 | self.execute( 30 | sql: self.adjustFluentQuery(query, SQLQueryConverter(delegate: SQLiteConverterDelegate()).convert(query)), 31 | { onOutput($0.databaseOutput()) } 32 | ) 33 | } 34 | 35 | func execute(query: DatabaseQuery, onOutput: @escaping @Sendable (any DatabaseOutput) -> Void) async throws { 36 | try await self.execute( 37 | sql: self.adjustFluentQuery(query, SQLQueryConverter(delegate: SQLiteConverterDelegate()).convert(query)), 38 | { onOutput($0.databaseOutput()) } 39 | ) 40 | } 41 | 42 | func execute(schema: DatabaseSchema) -> EventLoopFuture { 43 | var schema = schema 44 | 45 | if schema.action == .update { 46 | schema.updateFields = schema.updateFields.filter { 47 | switch $0 { // Filter out enum updates. 48 | case .dataType(_, .enum(_)): false 49 | default: true 50 | } 51 | } 52 | guard schema.createConstraints.isEmpty, schema.updateFields.isEmpty, schema.deleteConstraints.isEmpty else { 53 | return self.eventLoop.makeFailedFuture(FluentSQLiteUnsupportedAlter()) 54 | } 55 | if schema.createFields.isEmpty, schema.deleteFields.isEmpty { // If there were only enum updates, bail out. 56 | return self.eventLoop.makeSucceededFuture(()) 57 | } 58 | } 59 | 60 | return self.execute( 61 | sql: SQLSchemaConverter(delegate: SQLiteConverterDelegate()).convert(schema), 62 | { self.logger.debug("Unexpected row returned from schema query: \($0)") } 63 | ) 64 | } 65 | 66 | func execute(enum: DatabaseEnum) -> EventLoopFuture { 67 | self.eventLoop.makeSucceededFuture(()) 68 | } 69 | 70 | func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { 71 | self.eventLoop.makeFutureWithTask { try await self.withConnection { try await closure($0).get() } } 72 | } 73 | 74 | func withConnection(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { 75 | try await self.withConnection { 76 | try await closure( 77 | FluentSQLiteDatabase( 78 | database: $0, 79 | context: self.context, 80 | dataEncoder: self.dataEncoder, 81 | dataDecoder: self.dataDecoder, 82 | queryLogLevel: self.queryLogLevel, 83 | inTransaction: self.inTransaction 84 | )) 85 | } 86 | } 87 | 88 | func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture { 89 | self.inTransaction ? closure(self) : self.eventLoop.makeFutureWithTask { try await self.transaction { try await closure($0).get() } } 90 | } 91 | 92 | func transaction(_ closure: @escaping @Sendable (any Database) async throws -> T) async throws -> T { 93 | guard !self.inTransaction else { 94 | return try await closure(self) 95 | } 96 | 97 | return try await self.withConnection { conn in 98 | let db = FluentSQLiteDatabase( 99 | database: conn, 100 | context: self.context, 101 | dataEncoder: self.dataEncoder, 102 | dataDecoder: self.dataDecoder, 103 | queryLogLevel: self.queryLogLevel, 104 | inTransaction: true 105 | ) 106 | 107 | try await db.raw("BEGIN TRANSACTION").run() 108 | do { 109 | let result = try await closure(db) 110 | 111 | try await db.raw("COMMIT TRANSACTION").run() 112 | return result 113 | } catch { 114 | try? await db.raw("ROLLBACK TRANSACTION").run() 115 | throw error 116 | } 117 | } 118 | } 119 | 120 | // SQLDatabase 121 | 122 | var dialect: any SQLDialect { 123 | self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).dialect 124 | } 125 | 126 | var version: (any SQLDatabaseReportedVersion)? { 127 | self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).version 128 | } 129 | 130 | func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) -> EventLoopFuture { 131 | self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).execute(sql: query, onRow) 132 | } 133 | 134 | func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> Void) async throws { 135 | try await self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).execute( 136 | sql: query, onRow) 137 | } 138 | 139 | func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { 140 | try await self.database.sql(encoder: self.dataEncoder, decoder: self.dataDecoder, queryLogLevel: self.queryLogLevel).withSession(closure) 141 | } 142 | 143 | // SQLiteDatabase 144 | 145 | func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture { 146 | self.withConnection { $0.query(query, binds, logger: logger, onRow) } 147 | } 148 | 149 | func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture { 150 | self.database.withConnection(closure) 151 | } 152 | } 153 | 154 | private struct FluentSQLiteUnsupportedAlter: Error, CustomStringConvertible { 155 | var description: String { 156 | "SQLite only supports adding columns in ALTER TABLE statements." 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Tests/FluentSQLiteDriverTests/FluentSQLiteDriverTests.swift: -------------------------------------------------------------------------------- 1 | import FluentBenchmark 2 | import FluentKit 3 | import FluentSQL 4 | import FluentSQLiteDriver 5 | import Logging 6 | import NIO 7 | import SQLKit 8 | import SQLiteNIO 9 | import XCTest 10 | 11 | func XCTAssertThrowsErrorAsync( 12 | _ expression: @autoclosure () async throws -> T, 13 | _ message: @autoclosure () -> String = "", 14 | file: StaticString = #filePath, line: UInt = #line, 15 | _ callback: (any Error) -> Void = { _ in } 16 | ) async { 17 | do { 18 | _ = try await expression() 19 | XCTAssertThrowsError({}(), message(), file: file, line: line, callback) 20 | } catch { 21 | XCTAssertThrowsError(try { throw error }(), message(), file: file, line: line, callback) 22 | } 23 | } 24 | 25 | func XCTAssertNoThrowAsync( 26 | _ expression: @autoclosure () async throws -> T, 27 | _ message: @autoclosure () -> String = "", 28 | file: StaticString = #filePath, line: UInt = #line 29 | ) async { 30 | do { 31 | _ = try await expression() 32 | } catch { 33 | XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line) 34 | } 35 | } 36 | 37 | final class FluentSQLiteDriverTests: XCTestCase { 38 | func testAggregate() throws { try self.benchmarker.testAggregate() } 39 | func testArray() throws { try self.benchmarker.testArray() } 40 | func testBatch() throws { try self.benchmarker.testBatch() } 41 | func testChild() throws { try self.benchmarker.testChild() } 42 | func testChildren() throws { try self.benchmarker.testChildren() } 43 | func testCodable() throws { try self.benchmarker.testCodable() } 44 | func testChunk() throws { try self.benchmarker.testChunk() } 45 | func testCompositeID() throws { try self.benchmarker.testCompositeID() } 46 | func testCRUD() throws { try self.benchmarker.testCRUD() } 47 | func testEagerLoad() throws { try self.benchmarker.testEagerLoad() } 48 | func testEnum() throws { try self.benchmarker.testEnum() } 49 | func testFilter() throws { try self.benchmarker.testFilter() } 50 | func testGroup() throws { try self.benchmarker.testGroup() } 51 | func testID() throws { try self.benchmarker.testID() } 52 | func testJoin() throws { try self.benchmarker.testJoin() } 53 | func testMiddleware() throws { try self.benchmarker.testMiddleware() } 54 | func testMigrator() throws { try self.benchmarker.testMigrator() } 55 | func testModel() throws { try self.benchmarker.testModel() } 56 | func testOptionalParent() throws { try self.benchmarker.testOptionalParent() } 57 | func testPagination() throws { try self.benchmarker.testPagination() } 58 | func testParent() throws { try self.benchmarker.testParent() } 59 | func testPerformance() throws { try self.benchmarker.testPerformance() } 60 | func testRange() throws { try self.benchmarker.testRange() } 61 | func testSchema() throws { try self.benchmarker.testSchema() } 62 | func testSet() throws { try self.benchmarker.testSet() } 63 | func testSiblings() throws { try self.benchmarker.testSiblings() } 64 | func testSoftDelete() throws { try self.benchmarker.testSoftDelete() } 65 | func testSort() throws { try self.benchmarker.testSort() } 66 | func testSQL() throws { try self.benchmarker.testSQL() } 67 | func testTimestamp() throws { try self.benchmarker.testTimestamp() } 68 | func testTransaction() throws { try self.benchmarker.testTransaction() } 69 | func testUnique() throws { try self.benchmarker.testUnique() } 70 | 71 | func testDatabaseError() async throws { 72 | let sql = (self.database as! any SQLDatabase) 73 | 74 | await XCTAssertThrowsErrorAsync(try await sql.raw("asdf").run()) { 75 | XCTAssertTrue(($0 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: $0))") 76 | XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))") 77 | XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))") 78 | } 79 | 80 | try await sql.drop(table: "foo").ifExists().run() 81 | try await sql.create(table: "foo").column("name", type: .text, .unique).run() 82 | try await sql.insert(into: "foo").columns("name").values("bar").run() 83 | await XCTAssertThrowsErrorAsync(try await sql.insert(into: "foo").columns("name").values("bar").run()) { 84 | XCTAssertTrue(($0 as? any DatabaseError)?.isConstraintFailure ?? false, "\(String(reflecting: $0))") 85 | XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))") 86 | XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))") 87 | } 88 | } 89 | 90 | // https://github.com/vapor/fluent-sqlite-driver/issues/62 91 | func testUnsupportedUpdateMigration() async throws { 92 | struct UserMigration_v1_0_0: AsyncMigration { 93 | func prepare(on database: any Database) async throws { 94 | try await database.schema("users") 95 | .id() 96 | .field("email", .string, .required) 97 | .field("password", .string, .required) 98 | .unique(on: "email") 99 | .create() 100 | } 101 | 102 | func revert(on database: any Database) async throws { 103 | try await database.schema("users").delete() 104 | } 105 | } 106 | 107 | struct UserMigration_v1_2_0: AsyncMigration { 108 | func prepare(on database: any Database) async throws { 109 | try await database.schema("users") 110 | .field("apple_id", .string) 111 | .unique(on: "apple_id") 112 | .update() 113 | } 114 | 115 | func revert(on database: any Database) async throws { 116 | try await database.schema("users") 117 | .deleteUnique(on: "apple_id") 118 | .update() 119 | } 120 | } 121 | 122 | try await UserMigration_v1_0_0().prepare(on: self.database) 123 | await XCTAssertThrowsErrorAsync(try await UserMigration_v1_2_0().prepare(on: self.database)) { 124 | XCTAssert(String(describing: $0).contains("adding columns")) 125 | } 126 | await XCTAssertThrowsErrorAsync(try await UserMigration_v1_2_0().revert(on: self.database)) { 127 | XCTAssert(String(describing: $0).contains("adding columns")) 128 | } 129 | await XCTAssertNoThrowAsync(try await UserMigration_v1_0_0().revert(on: self.database)) 130 | } 131 | 132 | // https://github.com/vapor/fluent-sqlite-driver/issues/91 133 | func testDeleteFieldMigration() async throws { 134 | struct UserMigration_v1_0_0: AsyncMigration { 135 | func prepare(on database: any Database) async throws { 136 | try await database.schema("users").id().field("email", .string, .required).field("password", .string, .required).create() 137 | } 138 | func revert(on database: any Database) async throws { 139 | try await database.schema("users").delete() 140 | } 141 | } 142 | struct UserMigration_v1_1_0: AsyncMigration { 143 | func prepare(on database: any Database) async throws { 144 | try await database.schema("users").deleteField("password").update() 145 | } 146 | func revert(on database: any Database) async throws { 147 | try await database.schema("users").field("password", .string, .required).update() 148 | } 149 | } 150 | 151 | await XCTAssertNoThrowAsync(try await UserMigration_v1_0_0().prepare(on: self.database)) 152 | await XCTAssertNoThrowAsync(try await UserMigration_v1_1_0().prepare(on: self.database)) 153 | await XCTAssertNoThrowAsync(try await UserMigration_v1_1_0().revert(on: self.database)) 154 | await XCTAssertNoThrowAsync(try await UserMigration_v1_0_0().revert(on: self.database)) 155 | } 156 | 157 | func testCustomJSON() async throws { 158 | struct Metadata: Codable { let createdAt: Date } 159 | final class Event: Model, @unchecked Sendable { 160 | static let schema = "events" 161 | @ID(custom: "id", generatedBy: .database) var id: Int? 162 | @Field(key: "metadata") var metadata: Metadata 163 | } 164 | final class EventStringlyTyped: Model, @unchecked Sendable { 165 | static let schema = "events" 166 | @ID(custom: "id", generatedBy: .database) var id: Int? 167 | @Field(key: "metadata") var metadata: [String: String] 168 | } 169 | struct EventMigration: AsyncMigration { 170 | func prepare(on database: any Database) async throws { 171 | try await database.schema(Event.schema) 172 | .field("id", .int, .identifier(auto: false)) 173 | .field("metadata", .json, .required) 174 | .create() 175 | } 176 | func revert(on database: any Database) async throws { 177 | try await database.schema(Event.schema).delete() 178 | } 179 | } 180 | 181 | let jsonEncoder = JSONEncoder() 182 | jsonEncoder.dateEncodingStrategy = .iso8601 183 | let jsonDecoder = JSONDecoder() 184 | jsonDecoder.dateDecodingStrategy = .iso8601 185 | let iso8601 = DatabaseID(string: "iso8601") 186 | 187 | self.dbs.use(.sqlite(.memory, dataEncoder: .init(json: jsonEncoder), dataDecoder: .init(json: jsonDecoder)), as: iso8601) 188 | let db = self.dbs.database(iso8601, logger: .init(label: "test"), on: self.dbs.eventLoopGroup.any())! 189 | 190 | try await EventMigration().prepare(on: db) 191 | do { 192 | let date = Date() 193 | let event = Event() 194 | event.id = 1 195 | event.metadata = Metadata(createdAt: date) 196 | try await event.save(on: db) 197 | 198 | let rows = try await EventStringlyTyped.query(on: db).filter(\.$id == 1).all() 199 | XCTAssertEqual(rows[0].metadata["createdAt"], ISO8601DateFormatter().string(from: date)) 200 | } catch { 201 | try? await EventMigration().revert(on: db) 202 | throw error 203 | } 204 | try await EventMigration().revert(on: db) 205 | } 206 | 207 | var benchmarker: FluentBenchmarker { .init(databases: self.dbs) } 208 | var database: (any Database)! 209 | var dbs: Databases! 210 | let benchmarkPath = FileManager.default.temporaryDirectory.appendingPathComponent("benchmark.sqlite").absoluteString 211 | 212 | override class func setUp() { 213 | XCTAssert(isLoggingConfigured) 214 | } 215 | 216 | override func setUpWithError() throws { 217 | try super.setUpWithError() 218 | self.dbs = Databases(threadPool: NIOThreadPool.singleton, on: MultiThreadedEventLoopGroup.singleton) 219 | self.dbs.use(.sqlite(.memory, connectionPoolTimeout: .seconds(30)), as: .sqlite) 220 | self.dbs.use(.sqlite(.file(self.benchmarkPath), connectionPoolTimeout: .seconds(30)), as: .init(string: "benchmark")) 221 | self.database = self.dbs.database(.sqlite, logger: .init(label: "test.fluent.sqlite"), on: MultiThreadedEventLoopGroup.singleton.any()) 222 | } 223 | 224 | override func tearDown() async throws { 225 | await self.dbs.shutdownAsync() 226 | self.dbs = nil 227 | try await super.tearDown() 228 | } 229 | } 230 | 231 | func env(_ name: String) -> String? { 232 | ProcessInfo.processInfo.environment[name] 233 | } 234 | 235 | let isLoggingConfigured: Bool = { 236 | LoggingSystem.bootstrap { label in 237 | var handler = StreamLogHandler.standardOutput(label: label) 238 | handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .debug 239 | return handler 240 | } 241 | return true 242 | }() 243 | --------------------------------------------------------------------------------