diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
deleted file mode 100644
index 4fd472f..0000000
--- a/.github/CONTRIBUTING.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Contributing to Fluent's MySQL Driver
-
-👋 Welcome to the Vapor team!
-
-## Database
-
-In order to build and test against Postgres, you will need a database running. The easiest way to do this is using Docker and the included `docker-compose.yml` file.
-
-If you have Docker installed on your computer, all you will need to do is:
-
-```fish
-docker-compose mysql-a mysql-b
-# or for mariadb
-docker-compose mariadb-a mariadb-b
-```
-
-This will start the two databases required for running this package's unit tests. One of the databases will run on MySQL's default port 3306 and the other on 3307.
-
-### Environment Variables
-
-You will need to set the following environment variable to run the tests:
-
-```
-MYSQL_PORT_B=3307
-```
-
-This tells the tests that the second database is running on a different port.
-
-You may also set the log level variable to increase or decrease logging:
-
-```
-LOG_LEVEL=notice
-```
-
-In Xcode, edit the `fluent-mysql-driver` scheme to add env variables. In the terminal, use `export`.
-
-## Xcode
-
-To open the project in Xcode:
-
-- Clone the repo to your computer
-- Drag and drop the folder onto Xcode
-
-You can then run the unit tests in Xcode by pressing `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/workflows/api-docs.yml b/.github/workflows/api-docs.yml
index 3013058..81c71aa 100644
--- a/.github/workflows/api-docs.yml
+++ b/.github/workflows/api-docs.yml
@@ -11,4 +11,4 @@ jobs:
with:
package_name: fluent-mysql-driver
modules: FluentMySQLDriver
- pathsToInvalidate: /fluentmysqldriver
+ pathsToInvalidate: /fluentmysqldriver/*
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a3333b9..8260660 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,15 +24,17 @@ env:
jobs:
api-breakage:
- if: ${{ !(github.event.pull_request.draft || false) }}
+ if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
- container: swift:5.8-jammy
+ container: swift:jammy
steps:
- - name: Check out package
- uses: actions/checkout@v3
+ - name: Checkout
+ uses: actions/checkout@v4
with: { 'fetch-depth': 0 }
- - name: Run API breakage check action
- uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@reusable-workflows
+ - name: API breaking changes
+ run: |
+ git config --global --add safe.directory "${GITHUB_WORKSPACE}"
+ swift package diagnose-api-breaking-changes origin/main
linux-unit:
if: ${{ !(github.event.pull_request.draft || false) }}
@@ -42,17 +44,17 @@ jobs:
dbimage:
- mysql:5.7
- mysql:8.0
+ - mysql:8.3
- mariadb:10.4
- mariadb:11
- percona:8.0
runner:
- - swift:5.6-focal
- - swift:5.7-jammy
- - swift:5.8-jammy
- - swiftlang/swift:nightly-5.9-jammy
- - swiftlang/swift:nightly-main-jammy
- runs-on: ubuntu-latest
+ # List is deliberately incomplete; we want to avoid running 50 jobs on every commit
+ - swift:5.8-focal
+ - swift:5.10-jammy
+ - swiftlang/swift:nightly-6.0-jammy
container: ${{ matrix.runner }}
+ runs-on: ubuntu-latest
services:
mysql-a:
image: ${{ matrix.dbimage }}
@@ -69,23 +71,14 @@ jobs:
MYSQL_PASSWORD: test_password
MYSQL_DATABASE: test_database
steps:
- - name: Display versions
- shell: bash
- run: |
- echo MYSQL_VERSION='${{ matrix.dbimage }}' >> $GITHUB_ENV
- if [[ '${{ contains(matrix.container, 'nightly') }}' == 'true' ]]; then
- SWIFT_PLATFORM="$(source /etc/os-release && echo "${ID}${VERSION_ID}")" SWIFT_VERSION="$(cat /.swift_tag)"
- printf 'SWIFT_PLATFORM=%s\nSWIFT_VERSION=%s\n' "${SWIFT_PLATFORM}" "${SWIFT_VERSION}" >>"${GITHUB_ENV}"
- fi
- printf 'OS: %s\nTag: %s\nVersion:\n' "${SWIFT_PLATFORM}-${RUNNER_ARCH}" "${SWIFT_VERSION}" && swift --version
- name: Check out package
- uses: actions/checkout@v3
- - name: Run tests with Thread Sanitizer and coverage
- run: swift test --sanitize=thread --enable-code-coverage
+ uses: actions/checkout@v4
+ - name: Run local tests with coverage and TSan
+ run: swift test --enable-code-coverage --sanitize=thread
- name: Submit coverage report to Codecov.io
- uses: vapor/swift-codecov-action@v0.2
+ uses: vapor/swift-codecov-action@v0.3
with:
- cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,MYSQL_VERSION'
+ codecov_token: ${{ secrets.CODECOV_TOKEN }}
macos-unit:
if: ${{ !(github.event.pull_request.draft || false) }}
@@ -93,19 +86,20 @@ jobs:
fail-fast: false
matrix:
include:
- - dbimage: mysql@8.0
- macos: macos-13
- xcode: latest-stable
- runs-on: ${{ matrix.macos }}
+ - macos-version: macos-13
+ xcode-version: '~14.3'
+ - macos-version: macos-14
+ xcode-version: latest
+ runs-on: ${{ matrix.macos-version }}
steps:
- name: Select latest available Xcode
uses: maxim-lobanov/setup-xcode@v1
- with:
- xcode-version: ${{ matrix.xcode }}
+ with:
+ xcode-version: ${{ matrix.xcode-version }}
- name: Install MySQL server from Homebrew
- run: brew install ${{ matrix.dbimage }} && brew link --force ${{ matrix.dbimage }}
+ run: brew install mysql && brew link --force mysql
- name: Start MySQL server
- run: brew services start ${{ matrix.dbimage }}
+ run: brew services start mysql
- name: Wait for MySQL server to be ready
run: until echo | mysql -uroot; do sleep 1; done
timeout-minutes: 5
@@ -117,7 +111,7 @@ jobs:
CREATE DATABASE test_database_b; GRANT ALL PRIVILEGES ON test_database_b.* TO test_username@localhost;
SQL
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Run tests with Thread Sanitizer
run: swift test --sanitize=thread
env:
diff --git a/Package.swift b/Package.swift
index 38db65c..98404c7 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.6
+// swift-tools-version:5.8
import PackageDescription
let package = Package(
@@ -13,20 +13,35 @@ let package = Package(
.library(name: "FluentMySQLDriver", targets: ["FluentMySQLDriver"]),
],
dependencies: [
- .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.43.0"),
+ .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.1"),
.package(url: "https://github.com/vapor/mysql-kit.git", from: "4.7.1"),
- .package(url: "https://github.com/apple/swift-log.git", from: "1.5.2"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
],
targets: [
- .target(name: "FluentMySQLDriver", dependencies: [
- .product(name: "FluentKit", package: "fluent-kit"),
- .product(name: "FluentSQL", package: "fluent-kit"),
- .product(name: "Logging", package: "swift-log"),
- .product(name: "MySQLKit", package: "mysql-kit"),
- ]),
- .testTarget(name: "FluentMySQLDriverTests", dependencies: [
- .product(name: "FluentBenchmark", package: "fluent-kit"),
- .target(name: "FluentMySQLDriver"),
- ]),
+ .target(
+ name: "FluentMySQLDriver",
+ dependencies: [
+ .product(name: "FluentKit", package: "fluent-kit"),
+ .product(name: "FluentSQL", package: "fluent-kit"),
+ .product(name: "Logging", package: "swift-log"),
+ .product(name: "MySQLKit", package: "mysql-kit"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "FluentMySQLDriverTests",
+ dependencies: [
+ .product(name: "FluentBenchmark", package: "fluent-kit"),
+ .target(name: "FluentMySQLDriver"),
+ ],
+ swiftSettings: swiftSettings
+ ),
]
)
+
+var swiftSettings: [SwiftSetting] { [
+ .enableUpcomingFeature("ConciseMagicFile"),
+ .enableUpcomingFeature("ForwardTrailingClosures"),
+ .enableUpcomingFeature("DisableOutwardActorInference"),
+ .enableExperimentalFeature("StrictConcurrency=complete"),
+] }
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..b52199f
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,48 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let package = Package(
+ name: "fluent-mysql-driver",
+ platforms: [
+ .macOS(.v10_15),
+ .iOS(.v13),
+ .watchOS(.v6),
+ .tvOS(.v13),
+ ],
+ products: [
+ .library(name: "FluentMySQLDriver", targets: ["FluentMySQLDriver"]),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/vapor/fluent-kit.git", from: "1.48.1"),
+ .package(url: "https://github.com/vapor/mysql-kit.git", from: "4.7.1"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
+ ],
+ targets: [
+ .target(
+ name: "FluentMySQLDriver",
+ dependencies: [
+ .product(name: "FluentKit", package: "fluent-kit"),
+ .product(name: "FluentSQL", package: "fluent-kit"),
+ .product(name: "Logging", package: "swift-log"),
+ .product(name: "MySQLKit", package: "mysql-kit"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "FluentMySQLDriverTests",
+ dependencies: [
+ .product(name: "FluentBenchmark", package: "fluent-kit"),
+ .target(name: "FluentMySQLDriver"),
+ ],
+ 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 3870cf4..fce6a78 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,35 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+FluentMySQLDriver is a [FluentKit] driver for MySQL clients. It provides support for using the Fluent ORM with MySQL databases, and uses [MySQLKit] to provide [SQLKit] driver services, [MySQLNIO] to connect and communicate with the database server asynchronously, and [AsyncKit] to provide connection pooling.
+
+[FluentKit]: https://github.com/vapor/fluent-kit
+[SQLKit]: https://github.com/vapor/sql-kit
+[MySQLKit]: https://github.com/vapor/mysql-kit
+[MySQLNIO]: https://github.com/vapor/mysql-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-mysql-driver.git", from: "4.0.0")
+```
+
+For additional information, see [the Fluent documentation](https://docs.vapor.codes/fluent/overview/).
diff --git a/Sources/FluentMySQLDriver/Docs.docc/Resources/vapor-fluentmysqldriver-logo.svg b/Sources/FluentMySQLDriver/Docs.docc/Resources/vapor-fluentmysqldriver-logo.svg
new file mode 100644
index 0000000..a144562
--- /dev/null
+++ b/Sources/FluentMySQLDriver/Docs.docc/Resources/vapor-fluentmysqldriver-logo.svg
@@ -0,0 +1,21 @@
+
diff --git a/Sources/FluentMySQLDriver/Docs.docc/index.md b/Sources/FluentMySQLDriver/Docs.docc/index.md
index 8ef17c8..187ad16 100644
--- a/Sources/FluentMySQLDriver/Docs.docc/index.md
+++ b/Sources/FluentMySQLDriver/Docs.docc/index.md
@@ -1,3 +1,14 @@
# ``FluentMySQLDriver``
-FluentMySQLDriver is a package to integrate MySQLNIO and and MySQLKit with FluentKit to make it easy to use and write MySQL database operations in Swift.
\ No newline at end of file
+FluentMySQLDriver is a [FluentKit] driver for MySQL clients.
+
+## Overview
+
+FluentMySQLDriver provides support for using the Fluent ORM with MySQL databases. It uses [MySQLKit] to provide [SQLKit] driver services, [MySQLNIO] to connect and communicate with the database server asynchronously, and [AsyncKit] to provide connection pooling.
+
+[FluentKit]: https://github.com/vapor/fluent-kit
+[SQLKit]: https://github.com/vapor/sql-kit
+[MySQLKit]: https://github.com/vapor/mysql-kit
+[MySQLNIO]: https://github.com/vapor/mysql-nio
+[AsyncKit]: https://github.com/vapor/async-kit
+
diff --git a/Sources/FluentMySQLDriver/Docs.docc/theme-settings.json b/Sources/FluentMySQLDriver/Docs.docc/theme-settings.json
new file mode 100644
index 0000000..d01225e
--- /dev/null
+++ b/Sources/FluentMySQLDriver/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": {
+ "mysql-turquoise": "#02758f",
+ "documentation-intro-fill": "radial-gradient(circle at top, var(--color-mysql-turquoise) 30%, #000 100%)",
+ "documentation-intro-accent": "var(--color-mysql-turquoise)",
+ "logo-base": { "dark": "#fff", "light": "#000" },
+ "logo-shape": { "dark": "#000", "light": "#fff" },
+ "fill": { "dark": "#000", "light": "#fff" }
+ },
+ "icons": { "technology": "/mysqlkit/images/vapor-fluentmysqldriver-logo.svg" }
+ },
+ "features": {
+ "quickNavigation": { "enable": true },
+ "i18n": { "enable": true }
+ }
+}
diff --git a/Sources/FluentMySQLDriver/Exports.swift b/Sources/FluentMySQLDriver/Exports.swift
index 213b419..da47a8f 100644
--- a/Sources/FluentMySQLDriver/Exports.swift
+++ b/Sources/FluentMySQLDriver/Exports.swift
@@ -1,5 +1,3 @@
-#if swift(>=5.8)
-
@_documentation(visibility: internal) @_exported import FluentKit
@_documentation(visibility: internal) @_exported import struct Foundation.URL
@@ -18,30 +16,9 @@
@_documentation(visibility: internal) @_exported import struct NIOSSL.TLSConfiguration
-#else
-
-@_exported import FluentKit
-
-@_exported import struct Foundation.URL
-
-@_exported import struct MySQLKit.MySQLConfiguration
-@_exported import struct MySQLKit.MySQLConnectionSource
-@_exported import struct MySQLKit.MySQLDataEncoder
-@_exported import struct MySQLKit.MySQLDataDecoder
-
-@_exported import class MySQLNIO.MySQLConnection
-@_exported import enum MySQLNIO.MySQLError
-@_exported import struct MySQLNIO.MySQLData
-@_exported import protocol MySQLNIO.MySQLDatabase
-@_exported import protocol MySQLNIO.MySQLDataConvertible
-@_exported import struct MySQLNIO.MySQLRow
-
-@_exported import struct NIOSSL.TLSConfiguration
-
-#endif
-
extension DatabaseID {
+ /// A default `DatabaseID` to use for MySQL databases.
public static var mysql: DatabaseID {
- return .init(string: "mysql")
+ .init(string: "mysql")
}
}
diff --git a/Sources/FluentMySQLDriver/FluentMySQLConfiguration.swift b/Sources/FluentMySQLDriver/FluentMySQLConfiguration.swift
index 4b0b8d7..eea98fd 100644
--- a/Sources/FluentMySQLDriver/FluentMySQLConfiguration.swift
+++ b/Sources/FluentMySQLDriver/FluentMySQLConfiguration.swift
@@ -4,6 +4,18 @@ import FluentKit
import MySQLKit
extension DatabaseConfigurationFactory {
+ /// Create a database configuration factory for connecting to a server through a UNIX domain socket.
+ ///
+ /// - Parameters:
+ /// - unixDomainSocketPath: The path to the UNIX domain socket to connect through.
+ /// - username: The username to use for the connection.
+ /// - password: The password (empty string for none) to use for the connection.
+ /// - database: The default database for the connection, if any.
+ /// - maxConnectionsPerEventLoop: The maximum number of database connections to add to each event loop's pool.
+ /// - connectionPoolTimeout: The timeout for queries on the connection pool's wait list.
+ /// - encoder: A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
+ /// - decoder: A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
+ /// - Returns: An appropriate configuration factory.
public static func mysql(
unixDomainSocketPath: String,
username: String,
@@ -28,6 +40,17 @@ extension DatabaseConfigurationFactory {
decoder: decoder
)
}
+
+ /// Create a database configuration factory from an appropriately formatted URL string.
+ ///
+ /// - Parameters:
+ /// - url: A URL-formatted MySQL connection string. See `MySQLConfiguration` in MySQLKit for details of
+ /// accepted URL formats.
+ /// - maxConnectionsPerEventLoop: The maximum number of database connections to add to each event loop's pool.
+ /// - connectionPoolTimeout: The timeout for queries on the connection pool's wait list.
+ /// - encoder: A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
+ /// - decoder: A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
+ /// - Returns: An appropriate configuration factory.
public static func mysql(
url urlString: String,
maxConnectionsPerEventLoop: Int = 1,
@@ -47,6 +70,16 @@ extension DatabaseConfigurationFactory {
)
}
+ /// Create a database configuration factory from an appropriately formatted URL string.
+ ///
+ /// - Parameters:
+ /// - url: A `URL` containing MySQL connection parameters. See `MySQLConfiguration` in MySQLKit for details of
+ /// accepted URL formats.
+ /// - maxConnectionsPerEventLoop: The maximum number of database connections to add to each event loop's pool.
+ /// - connectionPoolTimeout: The timeout for queries on the connection pool's wait list.
+ /// - encoder: A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
+ /// - decoder: A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
+ /// - Returns: An appropriate configuration factory.
public static func mysql(
url: URL,
maxConnectionsPerEventLoop: Int = 1,
@@ -66,6 +99,20 @@ extension DatabaseConfigurationFactory {
)
}
+ /// Create a database configuration factory for connecting to a server with a hostname and optional port.
+ ///
+ /// - Parameters:
+ /// - hostname: The hostname to connect to.
+ /// - port: A TCP port number to connect on. Defaults to the IANA-assigned MySQL port number (3306).
+ /// - username: The username to use for the connection.
+ /// - password: The password (empty string for none) to use for the connection.
+ /// - database: The default database for the connection, if any.
+ /// - tlsConfiguration: An optional `TLSConfiguration` specifying encryption for the connection.
+ /// - maxConnectionsPerEventLoop: The maximum number of database connections to add to each event loop's pool.
+ /// - connectionPoolTimeout: The timeout for queries on the connection pool's wait list.
+ /// - encoder: A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
+ /// - decoder: A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
+ /// - Returns: An appropriate configuration factory.
public static func mysql(
hostname: String,
port: Int = 3306,
@@ -78,7 +125,7 @@ extension DatabaseConfigurationFactory {
encoder: MySQLDataEncoder = .init(),
decoder: MySQLDataDecoder = .init()
) -> Self {
- return .mysql(
+ .mysql(
configuration: .init(
hostname: hostname,
port: port,
@@ -94,6 +141,15 @@ extension DatabaseConfigurationFactory {
)
}
+ /// Create a database configuration factory for connecting to a server with a given `MySQLConfiguration`.
+ ///
+ /// - Parameters:
+ /// - configuration: A connection configuration.
+ /// - maxConnectionsPerEventLoop: The maximum number of database connections to add to each event loop's pool.
+ /// - connectionPoolTimeout: The timeout for queries on the connection pool's wait list.
+ /// - encoder: A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
+ /// - decoder: A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
+ /// - Returns: An appropriate configuration factory.
public static func mysql(
configuration: MySQLConfiguration,
maxConnectionsPerEventLoop: Int = 1,
@@ -101,7 +157,7 @@ extension DatabaseConfigurationFactory {
encoder: MySQLDataEncoder = .init(),
decoder: MySQLDataDecoder = .init()
) -> Self {
- return Self {
+ Self {
FluentMySQLConfiguration(
configuration: configuration,
maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
@@ -114,15 +170,28 @@ extension DatabaseConfigurationFactory {
}
}
+/// An implementation of `DatabaseConfiguration` for MySQL configurations.
struct FluentMySQLConfiguration: DatabaseConfiguration {
+ /// The underlying `MySQLConfiguration`.
let configuration: MySQLConfiguration
+
+ /// The maximum number of database connections to add to each event loop's pool.
let maxConnectionsPerEventLoop: Int
+
+ /// The timeout for queries on the connection pool's wait list.
let connectionPoolTimeout: TimeAmount
+
+ /// A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
let encoder: MySQLDataEncoder
- let decoder: MySQLDataDecoder
- var middleware: [AnyModelMiddleware]
- func makeDriver(for databases: Databases) -> DatabaseDriver {
+ /// A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
+ let decoder: MySQLDataDecoder
+
+ // See `DatabaseConfiguration.middleware`.
+ var middleware: [any AnyModelMiddleware]
+
+ // See `DatabaseConfiguration.makeDriver(for:)`.
+ func makeDriver(for databases: Databases) -> any DatabaseDriver {
let db = MySQLConnectionSource(
configuration: self.configuration
)
diff --git a/Sources/FluentMySQLDriver/FluentMySQLDatabase.swift b/Sources/FluentMySQLDriver/FluentMySQLDatabase.swift
index ddbbc46..7a47552 100644
--- a/Sources/FluentMySQLDriver/FluentMySQLDatabase.swift
+++ b/Sources/FluentMySQLDriver/FluentMySQLDatabase.swift
@@ -3,18 +3,40 @@ import MySQLKit
import MySQLNIO
import AsyncKit
-struct _FluentMySQLDatabase {
- let database: MySQLDatabase
+/// A wrapper for a `MySQLDatabase` which provides `Database`, `SQLDatabase`, and forwarding `MySQLDatabase`
+/// conformances.
+struct _FluentMySQLDatabase: Database, SQLDatabase, MySQLDatabase {
+ /// A trivial wrapper type to work around Sendable warnings due to MySQLNIO not being Sendable-correct.
+ struct FakeSendable: @unchecked Sendable { let value: T }
+
+ /// The underlying database connection.
+ let database: FakeSendable
+
+ /// A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
let encoder: MySQLDataEncoder
+
+ /// A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
let decoder: MySQLDataDecoder
+
+ /// The `DatabaseContext` associated with this connection.
let context: DatabaseContext
+
+ /// Whether this is a transaction-specific connection.
let inTransaction: Bool
-}
+
+ /// Create a ``_FluentMySQLDatabase``.
+ init(database: any MySQLDatabase, encoder: MySQLDataEncoder, decoder: MySQLDataDecoder, context: DatabaseContext, inTransaction: Bool) {
+ self.database = .init(value: database)
+ self.encoder = encoder
+ self.decoder = decoder
+ self.context = context
+ self.inTransaction = inTransaction
+ }
-extension _FluentMySQLDatabase: Database {
+ // See `Database.execute(query:onOutput:)`.
func execute(
query: DatabaseQuery,
- onOutput: @escaping (DatabaseOutput) -> ()
+ onOutput: @escaping @Sendable (any DatabaseOutput) -> ()
) -> EventLoopFuture {
let expression = SQLQueryConverter(delegate: MySQLConverterDelegate())
.convert(query)
@@ -29,7 +51,7 @@ extension _FluentMySQLDatabase: Database {
switch query.action {
case .create where query.customIDKey != .string(""):
let row = LastInsertRow(
- metadata: metadata,
+ lastInsertID: metadata.lastInsertID,
customIDKey: query.customIDKey
)
onOutput(row)
@@ -41,29 +63,36 @@ extension _FluentMySQLDatabase: Database {
return self.eventLoop.makeFailedFuture(error)
}
}
-
+
+ /// This is here because it allows for full test coverage; it serves no actual purpose functionally.
+ /*private*/ func ignoreRow(_: MySQLRow) throws {}
+
+ // See `Database.execute(schema:)`.
func execute(schema: DatabaseSchema) -> EventLoopFuture {
let expression = SQLSchemaConverter(delegate: MySQLConverterDelegate())
.convert(schema)
let (sql, binds) = self.serialize(expression)
do {
- return try self.query(sql, binds.map { try MySQLDataEncoder().encode($0) }, onRow: {
- fatalError("unexpected row: \($0)")
- })
+ // Again, this is here purely for the benefit of coverage. It optimizes out as a no-op even in debug.
+ try? self.ignoreRow(.init(format: .binary, columnDefinitions: [], values: []))
+
+ return try self.query(sql, binds.map { try MySQLDataEncoder().encode($0) }, onRow: self.ignoreRow(_:))
} catch {
return self.eventLoop.makeFailedFuture(error)
}
}
+ // See `Database.execute(enum:)`.
func execute(enum: DatabaseEnum) -> EventLoopFuture {
self.eventLoop.makeSucceededFuture(())
}
- func transaction(_ closure: @escaping (Database) -> EventLoopFuture) -> EventLoopFuture {
+ // See `Database.transaction(_:)`.
+ func transaction(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
guard !self.inTransaction else {
return closure(self)
}
- return self.database.withConnection { conn in
+ return self.database.value.withConnection { conn in
conn.simpleQuery("START TRANSACTION").flatMap { _ in
let db = _FluentMySQLDatabase(
database: conn,
@@ -73,7 +102,7 @@ extension _FluentMySQLDatabase: Database {
inTransaction: true
)
return closure(db).flatMap { result in
- conn.simpleQuery("COMMIT").map { _ in
+ conn.simpleQuery("COMMIT").and(value: result).map { _, result in
result
}
}.flatMapError { error in
@@ -85,8 +114,9 @@ extension _FluentMySQLDatabase: Database {
}
}
- func withConnection(_ closure: @escaping (Database) -> EventLoopFuture) -> EventLoopFuture {
- self.database.withConnection {
+ // See `Database.withConnection(_:)`.
+ func withConnection(_ closure: @escaping @Sendable (any Database) -> EventLoopFuture) -> EventLoopFuture {
+ self.database.value.withConnection {
closure(_FluentMySQLDatabase(
database: $0,
encoder: self.encoder,
@@ -96,76 +126,117 @@ extension _FluentMySQLDatabase: Database {
))
}
}
-}
-
-extension _FluentMySQLDatabase: SQLDatabase {
- var dialect: SQLDialect {
- MySQLDialect()
+
+ // See `SQLDatabase.dialect`.
+ var dialect: any SQLDialect {
+ self.sql(encoder: self.encoder, decoder: self.decoder).dialect
+ }
+
+ // See `SQLDatabase.queryLogLevel`.
+ var queryLogLevel: Logger.Level? {
+ self.sql(encoder: self.encoder, decoder: self.decoder).queryLogLevel
}
- public func execute(
- sql query: SQLExpression,
- _ onRow: @escaping (SQLRow) -> ()
+ // See `SQLDatabase.execute(sql:_:)`.
+ func execute(
+ sql query: any SQLExpression,
+ _ onRow: @escaping @Sendable (any SQLRow) -> ()
) -> EventLoopFuture {
- self.sql().execute(sql: query, onRow)
+ self.sql(encoder: self.encoder, decoder: self.decoder).execute(sql: query, onRow)
+ }
+
+ // See `SQLDatabase.withSession(_:)`.
+ func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R {
+ try await self.withConnection { (conn: MySQLConnection) in
+ conn.eventLoop.makeFutureWithTask {
+ try await closure(conn.sql(encoder: self.encoder, decoder: self.decoder))
+ }
+ }.get()
}
-}
-extension _FluentMySQLDatabase: MySQLDatabase {
- func send(_ command: MySQLCommand, logger: Logger) -> EventLoopFuture {
- self.database.send(command, logger: logger)
+ // See `MySQLDatabase.send(_:logger:)`.
+ func send(_ command: any MySQLCommand, logger: Logger) -> EventLoopFuture {
+ self.database.value.send(command, logger: logger)
}
+ // See `MySQLDatabase.withConnection(_:)`.
func withConnection(_ closure: @escaping (MySQLConnection) -> EventLoopFuture) -> EventLoopFuture {
- self.database.withConnection(closure)
+ self.database.value.withConnection(closure)
}
}
-private struct LastInsertRow: DatabaseOutput {
+/// A `DatabaseOutput` used to provide last insert IDs from query metadata to the Fluent layer.
+/*private*/ struct LastInsertRow: DatabaseOutput {
+ // See `CustomStringConvertible.description`.
var description: String {
- "\(self.metadata)"
+ "\(self.lastInsertID.map { "\($0)" } ?? "nil")"
}
-
- let metadata: MySQLQueryMetadata
+
+ /// The last inserted ID as of the creation of this row.
+ let lastInsertID: UInt64?
+
+ /// If specified by the original query, an alternative to `FieldKey.id` to be considered valid.
let customIDKey: FieldKey?
-
- func schema(_ schema: String) -> DatabaseOutput {
+
+ // See `DatabaseOutput.schema(_:)`.
+ func schema(_ schema: String) -> any DatabaseOutput {
self
}
+ // See `DatabaseOutput.decodeNil(_:)`.
func decodeNil(_ key: FieldKey) throws -> Bool {
false
}
+ // See `DatabaseOutput.contains(_:)`.
func contains(_ key: FieldKey) -> Bool {
key == .id || key == self.customIDKey
}
- func decode(_ key: FieldKey, as type: T.Type) throws -> T
- where T: Decodable
- {
+ // See `DatabaseOutput.decode(_:as:)`.
+ func decode(_ key: FieldKey, as type: T.Type) throws -> T {
+ guard let lastInsertIDInitializable = T.self as? any LastInsertIDInitializable.Type else {
+ throw DecodingError.typeMismatch(T.self, .init(codingPath: [SomeCodingKey(stringValue: key.description)], debugDescription: "\(T.self) is not valid as a last insert ID"))
+ }
guard self.contains(key) else {
- fatalError("Cannot decode field from last insert row: \(key).")
+ throw DecodingError.keyNotFound(SomeCodingKey(stringValue: key.description), .init(codingPath: [], debugDescription: "LastInsertRow doesn't contain key \(key)"))
}
- if let lastInsertIDInitializable = T.self as? LastInsertIDInitializable.Type {
- return lastInsertIDInitializable.init(lastInsertID: self.metadata.lastInsertID!) as! T
- } else {
- fatalError("Unsupported database generated identifier type: \(T.self).")
+ guard let lastInsertID = self.lastInsertID else {
+ throw DecodingError.valueNotFound(T.self, .init(codingPath: [], debugDescription: "LastInsertRow received metadata with no last insert ID"))
}
+ return lastInsertIDInitializable.init(lastInsertID: lastInsertID) as! T
}
}
+/// Retroactive conformance to `Sendable` for `MySQLConnection`, which happens to actually be `Senable`-correct
+/// but not annotated as such.
+extension MySQLNIO.MySQLConnection: @unchecked Swift.Sendable {}
+
+/// Retroactive conformance to `Sendable` for `MySQLQueryMetadata`, which happens to actually be `Senable`-correct
+/// but not annotated as such.
+extension MySQLNIO.MySQLQueryMetadata: @unchecked Swift.Sendable {}
+
+/// A trivial protocol which identifies types that may be returned by MySQL as "last insert ID" values.
protocol LastInsertIDInitializable {
+ /// Create an instance of `Self` from a given unsigned 64-bit integer ID value.
init(lastInsertID: UInt64)
}
extension LastInsertIDInitializable where Self: FixedWidthInteger {
+ /// Default implementation of ``init(lastInsertID:)`` for `FixedWidthInteger`s.
init(lastInsertID: UInt64) {
self = numericCast(lastInsertID)
}
}
+/// `UInt64` is a valid last inserted ID value type.
extension UInt64: LastInsertIDInitializable { }
+
+/// `UInt` is a valid last inserted ID value type.
extension UInt: LastInsertIDInitializable { }
+
+/// `Int` is a valid last inserted ID value type.
extension Int: LastInsertIDInitializable { }
+
+/// `Int64` is a valid last inserted ID value type.
extension Int64: LastInsertIDInitializable { }
diff --git a/Sources/FluentMySQLDriver/FluentMySQLDriver.swift b/Sources/FluentMySQLDriver/FluentMySQLDriver.swift
index 59f6d58..d7bd91b 100644
--- a/Sources/FluentMySQLDriver/FluentMySQLDriver.swift
+++ b/Sources/FluentMySQLDriver/FluentMySQLDriver.swift
@@ -1,18 +1,21 @@
-import AsyncKit
+@preconcurrency import AsyncKit
import FluentKit
import MySQLKit
import NIOCore
+/// An implementation of `DatabaseDriver` for MySQL .
struct _FluentMySQLDriver: DatabaseDriver {
+ /// The connection pool set for this driver.
let pool: EventLoopGroupConnectionPool
+
+ /// A `MySQLDataEncoder` used to translate bound query parameters into `MySQLData` values.
let encoder: MySQLDataEncoder
+
+ /// A `MySQLDataDecoder` used to translate `MySQLData` values into output values in `SQLRow`s.
let decoder: MySQLDataDecoder
- var eventLoopGroup: EventLoopGroup {
- self.pool.eventLoopGroup
- }
-
- func makeDatabase(with context: DatabaseContext) -> Database {
+ // See `DatabaseDriver.makeDatabase(with:)`.
+ func makeDatabase(with context: DatabaseContext) -> any Database {
_FluentMySQLDatabase(
database: self.pool.pool(for: context.eventLoop).database(logger: context.logger),
encoder: self.encoder,
@@ -22,6 +25,7 @@ struct _FluentMySQLDriver: DatabaseDriver {
)
}
+ // See `DatabaseDriver.shutdown()`.
func shutdown() {
self.pool.shutdown()
}
diff --git a/Sources/FluentMySQLDriver/FluentMySQLError.swift b/Sources/FluentMySQLDriver/FluentMySQLError.swift
index 4ccb630..57cec92 100644
--- a/Sources/FluentMySQLDriver/FluentMySQLError.swift
+++ b/Sources/FluentMySQLDriver/FluentMySQLError.swift
@@ -1,3 +1,6 @@
+/// Errors that may be thrown by this package.
enum FluentMySQLError: Error {
+ /// ``FluentMySQLConfiguration/mysql(url:maxConnectionsPerEventLoop:connectionPoolTimeout:encoder:decoder:)`` was
+ /// invoked with an invalid input.
case invalidURL(String)
}
diff --git a/Sources/FluentMySQLDriver/MySQLConverterDelegate.swift b/Sources/FluentMySQLDriver/MySQLConverterDelegate.swift
index 981ddb7..dd64a52 100644
--- a/Sources/FluentMySQLDriver/MySQLConverterDelegate.swift
+++ b/Sources/FluentMySQLDriver/MySQLConverterDelegate.swift
@@ -1,7 +1,9 @@
import FluentSQL
+/// An implementation of `SQLConverterDelegate` for MySQL .
struct MySQLConverterDelegate: SQLConverterDelegate {
- func customDataType(_ dataType: DatabaseSchema.DataType) -> SQLExpression? {
+ // See `SQLConverterDelegate.customDataType(_:)`.
+ func customDataType(_ dataType: DatabaseSchema.DataType) -> (any SQLExpression)? {
switch dataType {
case .string: return SQLRaw("VARCHAR(255)")
case .datetime: return SQLRaw("DATETIME(6)")
@@ -12,6 +14,7 @@ struct MySQLConverterDelegate: SQLConverterDelegate {
}
}
+ // See `SQLConverterDelegate.beforeConvert(_:)`.
func beforeConvert(_ schema: DatabaseSchema) -> DatabaseSchema {
var copy = schema
// convert field foreign keys to table-level foreign keys
diff --git a/Sources/FluentMySQLDriver/MySQLDatabase.swift b/Sources/FluentMySQLDriver/MySQLDatabase.swift
deleted file mode 100644
index 07f33aa..0000000
--- a/Sources/FluentMySQLDriver/MySQLDatabase.swift
+++ /dev/null
@@ -1,2 +0,0 @@
-import SQLKit
-import AsyncKit
diff --git a/Sources/FluentMySQLDriver/MySQLError+Database.swift b/Sources/FluentMySQLDriver/MySQLError+Database.swift
index e23e116..f018286 100644
--- a/Sources/FluentMySQLDriver/MySQLError+Database.swift
+++ b/Sources/FluentMySQLDriver/MySQLError+Database.swift
@@ -1,7 +1,9 @@
import MySQLNIO
import FluentKit
+/// Conform `MySQLError` to `DatabaseError`.
extension MySQLError: DatabaseError {
+ // See `DatabaseError.isSyntaxError`.
public var isSyntaxError: Bool {
switch self {
case .invalidSyntax(_):
@@ -11,6 +13,7 @@ extension MySQLError: DatabaseError {
}
}
+ // See `DatabaseError.isConstraintFailure`.
public var isConstraintFailure: Bool {
switch self {
case .duplicateEntry(_):
@@ -20,6 +23,7 @@ extension MySQLError: DatabaseError {
}
}
+ // See `DatabaseError.isConnectionClosed`.
public var isConnectionClosed: Bool {
switch self {
case .closed:
diff --git a/Sources/FluentMySQLDriver/MySQLRow+Database.swift b/Sources/FluentMySQLDriver/MySQLRow+Database.swift
index 3d55b1a..f1ebd04 100644
--- a/Sources/FluentMySQLDriver/MySQLRow+Database.swift
+++ b/Sources/FluentMySQLDriver/MySQLRow+Database.swift
@@ -3,24 +3,37 @@ import MySQLKit
import FluentKit
extension MySQLRow {
- internal func databaseOutput(decoder: MySQLDataDecoder) -> DatabaseOutput {
+ /// Returns a `DatabaseOutput` for this row.
+ ///
+ /// - Parameter decoder: A `MySQLDataDecoder` used to translate `MySQLData` values into output values.
+ /// - Returns: A `DatabaseOutput` instance.
+ func databaseOutput(decoder: MySQLDataDecoder) -> any DatabaseOutput {
_MySQLDatabaseOutput(row: self, decoder: decoder, schema: nil)
}
}
+/// A `DatabaseOutput` implementation for `MySQLRow`s.
private struct _MySQLDatabaseOutput: DatabaseOutput {
+ /// The underlying row.
let row: MySQLRow
+
+ /// A `MySQLDataDecoder` used to translate `MySQLData` values into output values.
let decoder: MySQLDataDecoder
+
+ /// The most recently set schema value (see `DatabaseOutput.schema(_:)`).
let schema: String?
+ // See `DatabaseOutput.description`.
var description: String {
self.row.description
}
+ // See `DatabaseOutput.contains(_:)`.
func contains(_ key: FieldKey) -> Bool {
self.row.column(self.columnName(key)) != nil
}
+ // See `DatabaseOutput.decodeNil(_:)`.
func decodeNil(_ key: FieldKey) throws -> Bool {
if let data = self.row.column((self.columnName(key))) {
return data.buffer == nil
@@ -29,7 +42,8 @@ private struct _MySQLDatabaseOutput: DatabaseOutput {
}
}
- func schema(_ schema: String) -> DatabaseOutput {
+ // See `DatabaseOutput.schema(_:)`.
+ func schema(_ schema: String) -> any DatabaseOutput {
_MySQLDatabaseOutput(
row: self.row,
decoder: self.decoder,
@@ -37,15 +51,14 @@ private struct _MySQLDatabaseOutput: DatabaseOutput {
)
}
- func decode(_ key: FieldKey, as type: T.Type) throws -> T
- where T: Decodable
- {
+ // See `DatabaseOutput.decode(_:as:)`.
+ func decode(_ key: FieldKey, as type: T.Type) throws -> T {
try self.row
.sql(decoder: self.decoder)
.decode(column: self.columnName(key), as: T.self)
}
-
+ /// Translates a given `FieldKey` into a column name, accounting for the current schema, if any.
private func columnName(_ key: FieldKey) -> String {
if let schema = self.schema {
return "\(schema)_\(key.description)"
diff --git a/Tests/FluentMySQLDriverTests/FluentMySQLDriverTests.swift b/Tests/FluentMySQLDriverTests/FluentMySQLDriverTests.swift
index ddc757f..d21acae 100644
--- a/Tests/FluentMySQLDriverTests/FluentMySQLDriverTests.swift
+++ b/Tests/FluentMySQLDriverTests/FluentMySQLDriverTests.swift
@@ -1,6 +1,7 @@
import NIO
import FluentBenchmark
-import FluentMySQLDriver
+import FluentSQL
+@testable import FluentMySQLDriver
import SQLKit
import XCTest
import Logging
@@ -8,11 +9,52 @@ import MySQLKit
import MySQLNIO
import NIOSSL
+func XCTAssertEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Equatable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertEqual(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertNilAsync(
+ _ expression: @autoclosure () async throws -> Any?,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ let result = try await expression()
+ return XCTAssertNil(result, message(), file: file, line: line)
+ } catch {
+ return XCTAssertNil(try { throw error }(), message(), file: file, line: line)
+ }
+}
+
+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)
+ }
+}
+
final class FluentMySQLDriverTests: 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,99 +84,60 @@ final class FluentMySQLDriverTests: XCTestCase {
func testTransaction() throws { try self.benchmarker.testTransaction() }
func testUnique() throws { try self.benchmarker.testUnique() }
- func testDatabaseError() throws {
- let sql = (self.db 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.db 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.create(table: "foo").column("name", type: .text, .unique).run().wait()
- try sql.insert(into: "foo").columns("name").values("bar").run().wait()
- try sql.insert(into: "foo").columns("name").values("bar").run().wait()
- } catch let error as DatabaseError where error.isConstraintFailure {
- // 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))")
}
- do {
- try self.mysql.withConnection { conn in
- conn.close().flatMap {
- conn.sql().insert(into: "foo").columns("name").values("bar").run()
- }
- }.wait()
- } catch let error as DatabaseError where error.isConnectionClosed {
- // pass
- } catch let error {
- let error = error
- XCTFail("\(error)")
+ await XCTAssertThrowsErrorAsync(try await self.mysql.withConnection { conn in
+ conn.close().flatMap {
+ conn.sql().insert(into: "foo").columns("name").values("bar").run()
+ }
+ }.get()) {
+ XCTAssertTrue(($0 as? any DatabaseError)?.isConnectionClosed ?? false, "\(String(reflecting: $0))")
+ XCTAssertFalse(($0 as? any DatabaseError)?.isSyntaxError ?? true, "\(String(reflecting: $0))")
+ XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))")
}
}
- func testClarityModel() throws {
- final class Clarity: Model {
+ func testClarityModel() async throws {
+ final class Clarity: Model, @unchecked Sendable {
static let schema = "clarities"
- @ID(custom: .id, generatedBy: .database)
- var id: Int?
-
- @Field(key: "at")
- var at: Date
-
- @Field(key: "cloud_condition")
- var cloudCondition: Int
-
- @Field(key: "wind_condition")
- var windCondition: Int
-
- @Field(key: "rain_condition")
- var rainCondition: Int
-
- @Field(key: "day_condition")
- var daylightCondition: Int
-
- @Field(key: "sky_temperature")
- var skyTemperature: Double?
-
- @Field(key: "sensor_temperature")
- var sensorTemperature: Double?
-
- @Field(key: "ambient_temperature")
- var ambientTemperature: Double
-
- @Field(key: "dewpoint_temperature")
- var dewpointTemperature: Double
-
- @Field(key: "wind_speed")
- var windSpeed: Double?
-
- @Field(key: "humidity")
- var humidity: Double
-
- @Field(key: "daylight")
- var daylight: Int
-
- @Field(key: "rain")
- var rain: Bool
-
- @Field(key: "wet")
- var wet: Bool
-
- @Field(key: "heater")
- var heater: Double
-
- @Field(key: "close_requested")
- var closeRequested: Bool
-
- init() { }
+ @ID(custom: .id, generatedBy: .database) var id: Int?
+ @Field(key: "at") var at: Date
+ @Field(key: "cloud_condition") var cloudCondition: Int
+ @Field(key: "wind_condition") var windCondition: Int
+ @Field(key: "rain_condition") var rainCondition: Int
+ @Field(key: "day_condition") var daylightCondition: Int
+ @Field(key: "sky_temperature") var skyTemperature: Double?
+ @Field(key: "sensor_temperature") var sensorTemperature: Double?
+ @Field(key: "ambient_temperature") var ambientTemperature: Double
+ @Field(key: "dewpoint_temperature") var dewpointTemperature: Double
+ @Field(key: "wind_speed") var windSpeed: Double?
+ @Field(key: "humidity") var humidity: Double
+ @Field(key: "daylight") var daylight: Int
+ @Field(key: "rain") var rain: Bool
+ @Field(key: "wet") var wet: Bool
+ @Field(key: "heater") var heater: Double
+ @Field(key: "close_requested") var closeRequested: Bool
+
+ init() {}
}
- struct CreateClarity: Migration {
- func prepare(on database: Database) -> EventLoopFuture {
- return database.schema("clarities")
+ struct CreateClarity: AsyncMigration {
+ func prepare(on database: any Database) async throws {
+ try await database.schema("clarities")
.field("id", .int, .identifier(auto: true))
.field("at", .datetime, .required)
.field("cloud_condition", .int, .required)
@@ -155,16 +158,15 @@ final class FluentMySQLDriverTests: XCTestCase {
.create()
}
- func revert(on database: Database) -> EventLoopFuture {
- return database.schema("clarities").delete()
+ func revert(on database: any Database) async throws {
+ try await database.schema("clarities").delete()
}
}
- defer { try? CreateClarity().revert(on: self.db).wait() }
- try CreateClarity().prepare(on: self.db).wait()
+ try await CreateClarity().prepare(on: self.db)
- let now = Date()
do {
+ let now = Date()
let clarity = Clarity()
clarity.at = now
clarity.cloudCondition = 1
@@ -182,31 +184,34 @@ final class FluentMySQLDriverTests: XCTestCase {
clarity.wet = true
clarity.heater = 10
clarity.closeRequested = false
- try clarity.create(on: self.db).wait()
- }
- do {
- let clarity = try Clarity.query(on: self.db).first().wait()!
- XCTAssertEqual(clarity.at.description, now.description)
- XCTAssertEqual(clarity.cloudCondition, 1)
- XCTAssertEqual(clarity.windCondition, 2)
- XCTAssertEqual(clarity.rainCondition, 3)
- XCTAssertEqual(clarity.daylightCondition, 4)
- XCTAssertEqual(clarity.skyTemperature, nil)
- XCTAssertEqual(clarity.sensorTemperature, nil)
- XCTAssertEqual(clarity.ambientTemperature, 20.0)
- XCTAssertEqual(clarity.dewpointTemperature, -3.0)
- XCTAssertEqual(clarity.windSpeed, nil)
- XCTAssertEqual(clarity.humidity, 59.1)
- XCTAssertEqual(clarity.daylight, 12)
- XCTAssertEqual(clarity.rain, false)
- XCTAssertEqual(clarity.wet, true)
- XCTAssertEqual(clarity.heater, 10)
- XCTAssertEqual(clarity.closeRequested, false)
+ try await clarity.create(on: self.db)
+
+ let dbClarity = try await Clarity.query(on: self.db).first()
+ XCTAssertEqual(dbClarity?.at.description, now.description)
+ XCTAssertEqual(dbClarity?.cloudCondition, 1)
+ XCTAssertEqual(dbClarity?.windCondition, 2)
+ XCTAssertEqual(dbClarity?.rainCondition, 3)
+ XCTAssertEqual(dbClarity?.daylightCondition, 4)
+ XCTAssertEqual(dbClarity?.skyTemperature, nil)
+ XCTAssertEqual(dbClarity?.sensorTemperature, nil)
+ XCTAssertEqual(dbClarity?.ambientTemperature, 20.0)
+ XCTAssertEqual(dbClarity?.dewpointTemperature, -3.0)
+ XCTAssertEqual(dbClarity?.windSpeed, nil)
+ XCTAssertEqual(dbClarity?.humidity, 59.1)
+ XCTAssertEqual(dbClarity?.daylight, 12)
+ XCTAssertEqual(dbClarity?.rain, false)
+ XCTAssertEqual(dbClarity?.wet, true)
+ XCTAssertEqual(dbClarity?.heater, 10)
+ XCTAssertEqual(dbClarity?.closeRequested, false)
+ } catch {
+ try? await CreateClarity().revert(on: self.db)
+ throw error
}
+ try await CreateClarity().revert(on: self.db)
}
- func testBoolFilter() throws {
- final class Clarity: Model {
+ func testBoolFilter() async throws {
+ final class Clarity: Model, @unchecked Sendable {
static let schema = "clarities"
@ID(custom: .id, generatedBy: .database)
@@ -215,48 +220,47 @@ final class FluentMySQLDriverTests: XCTestCase {
@Field(key: "rain")
var rain: Bool
- init() { }
+ init() {}
init(rain: Bool) {
self.rain = rain
}
}
- struct CreateClarity: Migration {
- func prepare(on database: Database) -> EventLoopFuture {
- return database.schema("clarities")
+ struct CreateClarity: AsyncMigration {
+ func prepare(on database: any Database) async throws {
+ try await database.schema("clarities")
.field("id", .int, .identifier(auto: true))
.field("rain", .bool, .required)
.create()
}
- func revert(on database: Database) -> EventLoopFuture {
- return database.schema("clarities").delete()
+ func revert(on database: any Database) async throws {
+ try await database.schema("clarities").delete()
}
}
- defer { try? CreateClarity().revert(on: self.db).wait() }
- try CreateClarity().prepare(on: self.db).wait()
+ try await CreateClarity().prepare(on: self.db)
- let trueValue = Clarity(rain: true)
- let falseValue = Clarity(rain: false)
+ do {
+ let trueValue = Clarity(rain: true)
+ let falseValue = Clarity(rain: false)
- try trueValue.save(on: self.db).wait()
- try falseValue.save(on: self.db).wait()
+ try await trueValue.save(on: self.db)
+ try await falseValue.save(on: self.db)
- try XCTAssertEqual(Clarity.query(on: self.db).count().wait(), 2)
- try XCTAssertEqual(
- Clarity.query(on: self.db).filter(\.$rain == true).first().wait()?.id,
- trueValue.id
- )
- try XCTAssertEqual(
- Clarity.query(on: self.db).filter(\.$rain == false).first().wait()?.id,
- falseValue.id
- )
+ await XCTAssertEqualAsync(try await Clarity.query(on: self.db).count(), 2)
+ await XCTAssertEqualAsync(try await Clarity.query(on: self.db).filter(\.$rain == true).first()?.id, trueValue.id)
+ await XCTAssertEqualAsync(try await Clarity.query(on: self.db).filter(\.$rain == false).first()?.id, falseValue.id)
+ } catch {
+ try? await CreateClarity().revert(on: self.db)
+ throw error
+ }
+ try await CreateClarity().revert(on: self.db)
}
- func testDateDecoding() throws {
- final class Clarity: Model {
+ func testDateDecoding() async throws {
+ final class Clarity: Model, @unchecked Sendable {
static let schema = "clarities"
@ID(custom: .id, generatedBy: .database)
@@ -265,49 +269,54 @@ final class FluentMySQLDriverTests: XCTestCase {
@Field(key: "date")
var date: Date
- init() { }
+ init() {}
init(date: Date) {
self.date = date
}
}
- struct CreateClarity: Migration {
- func prepare(on database: Database) -> EventLoopFuture {
- return database.schema("clarities")
+ struct CreateClarity: AsyncMigration {
+ func prepare(on database: any Database) async throws {
+ try await database.schema("clarities")
.field("id", .int, .identifier(auto: true))
.field("date", .date, .required)
.create()
}
- func revert(on database: Database) -> EventLoopFuture {
- return database.schema("clarities").delete()
+ func revert(on database: any Database) async throws {
+ try await database.schema("clarities").delete()
}
}
- defer { try? CreateClarity().revert(on: self.db).wait() }
- try CreateClarity().prepare(on: self.db).wait()
-
- let formatter = DateFormatter()
- formatter.timeZone = TimeZone(secondsFromGMT: 0)!
- formatter.dateFormat = "yyyy-MM-dd"
-
- let firstDate = formatter.date(from: "2020-01-01")!
- let secondDate = formatter.date(from: "1994-05-23")!
- let trueValue = Clarity(date: firstDate)
- let falseValue = Clarity(date: secondDate)
-
- try trueValue.save(on: self.db).wait()
- try falseValue.save(on: self.db).wait()
-
- let receivedModels = try Clarity.query(on: self.db).all().wait()
- XCTAssertEqual(receivedModels.count, 2)
- XCTAssertEqual(receivedModels[0].date, firstDate)
- XCTAssertEqual(receivedModels[1].date, secondDate)
+ try await CreateClarity().prepare(on: self.db)
+
+ do {
+ let formatter = DateFormatter()
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)!
+ formatter.dateFormat = "yyyy-MM-dd"
+
+ let firstDate = formatter.date(from: "2020-01-01")!
+ let secondDate = formatter.date(from: "1994-05-23")!
+ let trueValue = Clarity(date: firstDate)
+ let falseValue = Clarity(date: secondDate)
+
+ try await trueValue.save(on: self.db)
+ try await falseValue.save(on: self.db)
+
+ let receivedModels = try await Clarity.query(on: self.db).all()
+ XCTAssertEqual(receivedModels.count, 2)
+ XCTAssertEqual(receivedModels[0].date, firstDate)
+ XCTAssertEqual(receivedModels[1].date, secondDate)
+ } catch {
+ try? await CreateClarity().revert(on: self.db)
+ throw error
+ }
+ try await CreateClarity().revert(on: self.db)
}
- func testChar36UUID() throws {
- final class Foo: Model {
+ func testChar36UUID() async throws {
+ final class Foo: Model, @unchecked Sendable {
static let schema = "foos"
@ID(key: .id)
@@ -317,59 +326,138 @@ final class FluentMySQLDriverTests: XCTestCase {
var _bar: String
var bar: UUID {
- get {
- UUID(uuidString: self._bar)!
- }
- set {
- self._bar = newValue.uuidString
- }
+ get { UUID(uuidString: self._bar)! }
+ set { self._bar = newValue.uuidString }
}
- init() { }
+ init() {}
init(id: UUID? = nil, bar: UUID) {
self.id = id
self.bar = bar
}
}
-
- try self.db.schema("foos")
+
+ try await self.mysql.sql().drop(table: "foos").ifExists().run()
+ try await self.db.schema("foos")
.id()
.field("bar", .sql(raw: "CHAR(36)"), .required)
.create()
- .wait()
- defer {
- try! self.db.schema("foos").delete().wait()
- }
- let foo = Foo(bar: .init())
- try foo.create(on: self.db).wait()
- XCTAssertNotNil(foo.id)
+ do {
+ let foo = Foo(bar: .init())
+ try await foo.create(on: self.db)
+ XCTAssertNotNil(foo.id)
- let fetched = try Foo.find(foo.id, on: self.db).wait()
- XCTAssertEqual(fetched?.bar, foo.bar)
+ let fetched = try await Foo.find(foo.id, on: self.db)
+ XCTAssertEqual(fetched?.bar, foo.bar)
+ } catch {
+ try? await self.db.schema("foos").delete()
+ }
+ try await self.db.schema("foos").delete()
}
-
- var benchmarker: FluentBenchmarker {
- return .init(databases: self.dbs)
+
+ func testBindingEncodeFailures() async throws {
+ struct FailingDataType: Codable, MySQLDataConvertible, Equatable {
+ init() {}
+ init?(mysqlData: MySQLData) { nil }
+ var mysqlData: MySQLData? { nil }
+ }
+ final class M: Model, @unchecked Sendable {
+ static let schema = "s"
+ @ID var id
+ @Field(key: "f") var f: FailingDataType
+ init() { self.f = .init() }
+ }
+ await XCTAssertThrowsErrorAsync(try await M.query(on: self.db).filter(\.$f == .init()).all()) {
+ XCTAssertNotNil($0 as? EncodingError, String(reflecting: $0))
+ }
+
+ await XCTAssertThrowsErrorAsync(try await self.db.schema("s").field("f", .custom(SQLBind(FailingDataType()))).create()) {
+ XCTAssertNotNil($0 as? EncodingError, String(reflecting: $0))
+ }
}
- var eventLoopGroup: EventLoopGroup!
- var threadPool: NIOThreadPool!
- var dbs: Databases!
- var db: Database {
- self.benchmarker.database
+
+ func testMiscSQLDatabaseSupport() async throws {
+ XCTAssertEqual((self.db as? any SQLDatabase)?.queryLogLevel, .debug)
+ await XCTAssertEqualAsync(try await (self.db as? any SQLDatabase)?.withSession { $0.dialect.name }, (self.db as? any SQLDatabase)?.dialect.name)
+
+ final class M: Model, @unchecked Sendable {
+ static let schema = "s"
+ @ID var id
+ @OptionalField(key: "k") var k: Int?
+ init() {}
+ }
+ try await (self.db as? any SQLDatabase)?.drop(table: M.schema).ifExists().run()
+ try await (self.db as? any SQLDatabase)?.create(table: M.schema).column("id", type: .custom(SQLRaw("varbinary(16)")), .primaryKey(autoIncrement: false)).run()
+ await XCTAssertNilAsync(try await (self.db as? any SQLDatabase)?.select().column("id").from(M.schema).first(decodingFluent: M.self)?.k)
+ try await (self.db as? any SQLDatabase)?.drop(table: M.schema).run()
}
- var mysql: MySQLDatabase {
- self.db as! MySQLDatabase
+
+ func testLastInsertRow() {
+ XCTAssertNotNil(LastInsertRow(lastInsertID: 0, customIDKey: nil).description)
+ let row = LastInsertRow(lastInsertID: nil, customIDKey: nil)
+ XCTAssertNotNil(row.description)
+ XCTAssertEqual(row.schema("").description, row.description)
+ XCTAssertFalse(try row.decodeNil(.id))
+ XCTAssertThrowsError(try row.decode(.id, as: String.self)) {
+ guard case .typeMismatch(_, _) = $0 as? DecodingError else {
+ return XCTFail("Expected DecodingError.typeMismatch, but got \(String(reflecting: $0))")
+ }
+ }
+ XCTAssertThrowsError(try row.decode("foo", as: Int.self)) {
+ guard case .keyNotFound(let key, _) = $0 as? DecodingError else {
+ return XCTFail("Expected DecodingError.keyNotFound, but got \(String(reflecting: $0))")
+ }
+ XCTAssertEqual(key.stringValue, "foo")
+ }
+ XCTAssertThrowsError(try row.decode(.id, as: Int.self)) {
+ guard case .valueNotFound(_, _) = $0 as? DecodingError else {
+ return XCTFail("Expected DecodingError.valueNotFound, but got \(String(reflecting: $0))")
+ }
+ }
}
+
+ func testNeverInvokedDatabaseOutputCodePath() async throws {
+ final class M: Model, @unchecked Sendable {
+ static let schema = "s"
+ @ID var id
+ init() {}
+ }
+ try await (self.db as? any SQLDatabase)?.drop(table: M.schema).ifExists().run()
+ try await self.db.schema(M.schema).id().create()
+ do {
+ try await M().create(on: self.db)
+ try await self.db.execute(query: M.query(on: self.db).field(\.$id).query, onOutput: { XCTAssert((try? $0.decodeNil("not a real key")) ?? false) }).get()
+ } catch {
+ try? await self.db.schema(M.schema).delete()
+ throw error
+ }
+ try await self.db.schema(M.schema).delete()
+ }
+
+ func testMiscConfigMethods() {
+ XCTAssertNotNil(try DatabaseConfigurationFactory.mysql(unixDomainSocketPath: "/", username: "", password: ""))
+ XCTAssertNoThrow(try DatabaseConfigurationFactory.mysql(url: "mysql://user@host/db"))
+ XCTAssertThrowsError(try DatabaseConfigurationFactory.mysql(url: "notmysql://foo@bar"))
+ XCTAssertThrowsError(try DatabaseConfigurationFactory.mysql(url: "not$a$valid$url://"))
+ XCTAssertNoThrow(try DatabaseConfigurationFactory.mysql(url: URL(string: "mysql://user@host/db")!))
+ XCTAssertThrowsError(try DatabaseConfigurationFactory.mysql(url: URL(string: "notmysql://foo@bar")!))
+ XCTAssertEqual(DatabaseID.mysql.string, "mysql")
+ }
+
+ var benchmarker: FluentBenchmarker { .init(databases: self.dbs) }
+ var eventLoopGroup: any EventLoopGroup { MultiThreadedEventLoopGroup.singleton }
+ var threadPool: NIOThreadPool { NIOThreadPool.singleton }
+ var dbs: Databases!
+ var db: any Database { self.benchmarker.database }
+ var mysql: any MySQLDatabase { self.db as! any MySQLDatabase }
override func setUpWithError() throws {
try super.setUpWithError()
XCTAssert(isLoggingConfigured)
- self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
- self.threadPool = NIOThreadPool(numberOfThreads: System.coreCount)
- self.dbs = Databases(threadPool: threadPool, on: self.eventLoopGroup)
+ self.dbs = Databases(threadPool: self.threadPool, on: self.eventLoopGroup)
var tls = TLSConfiguration.makeClientConfiguration()
tls.certificateVerification = .none
@@ -398,8 +486,6 @@ final class FluentMySQLDriverTests: XCTestCase {
override func tearDownWithError() throws {
self.dbs.shutdown()
- try self.threadPool.syncShutdownGracefully()
- try self.eventLoopGroup.syncShutdownGracefully()
try super.tearDownWithError()
}
@@ -411,13 +497,13 @@ extension DatabaseID {
}
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) } ?? .info
return handler
}
return true
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 401c38e..0000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,40 +0,0 @@
-version: '3'
-
-services:
- mariadb-a:
- image: mariadb
- environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: "true"
- MYSQL_DATABASE: vapor_database
- MYSQL_USER: vapor_username
- MYSQL_PASSWORD: vapor_password
- ports:
- - 3306:3306
- mariadb-b:
- image: mariadb
- environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: "true"
- MYSQL_DATABASE: vapor_database
- MYSQL_USER: vapor_username
- MYSQL_PASSWORD: vapor_password
- ports:
- - 3307:3306
- mysql-a:
- image: mysql:8.0
- environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: "true"
- MYSQL_DATABASE: vapor_database
- MYSQL_USER: vapor_username
- MYSQL_PASSWORD: vapor_password
- ports:
- - 3306:3306
- mysql-b:
- image: mysql:8.0
- environment:
- MYSQL_ALLOW_EMPTY_PASSWORD: "true"
- MYSQL_DATABASE: vapor_database
- MYSQL_USER: vapor_username
- MYSQL_PASSWORD: vapor_password
- ports:
- - 3307:3306
-