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 @@
+
+
+
+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")
-}