├── .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 |
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 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------