├── .api-breakage
└── allowlist-branch-nested-subpath-expressions.txt
├── .github
├── CODEOWNERS
└── workflows
│ ├── api-docs.yml
│ └── test.yml
├── .gitignore
├── .spi.yml
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── MySQLKit
│ ├── ConnectionPool+MySQL.swift
│ ├── Docs.docc
│ ├── MySQLKit.md
│ ├── Resources
│ │ └── vapor-mysqlkit-logo.svg
│ └── theme-settings.json
│ ├── Exports.swift
│ ├── MySQLConfiguration.swift
│ ├── MySQLConnectionSource.swift
│ ├── MySQLDataDecoder.swift
│ ├── MySQLDataEncoder.swift
│ ├── MySQLDatabase+SQL.swift
│ ├── MySQLDialect.swift
│ ├── MySQLRow+SQL.swift
│ └── Optional+MySQLDataConvertible.swift
└── Tests
└── MySQLKitTests
└── MySQLKitTests.swift
/.api-breakage/allowlist-branch-nested-subpath-expressions.txt:
--------------------------------------------------------------------------------
1 | API breakage: func MySQLDialect.customDataType(for:) has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
2 | API breakage: var MySQLDialect.sharedSelectLockExpression has declared type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
3 | API breakage: accessor MySQLDialect.sharedSelectLockExpression.Get() has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
4 | API breakage: var MySQLDialect.exclusiveSelectLockExpression has declared type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
5 | API breakage: accessor MySQLDialect.exclusiveSelectLockExpression.Get() has return type change from SQLKit.SQLExpression? to (SQLKit.SQLExpression)?
6 |
7 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @gwynne
2 | /.github/CONTRIBUTING.md @gwynne @0xTim
3 | /.github/workflows/*.yml @gwynne @0xTim
4 | /.github/workflows/test.yml @gwynne
5 | /.spi.yml @gwynne @0xTim
6 | /.gitignore @gwynne @0xTim
7 | /LICENSE @gwynne @0xTim
8 | /README.md @gwynne @0xTim
9 |
--------------------------------------------------------------------------------
/.github/workflows/api-docs.yml:
--------------------------------------------------------------------------------
1 | name: deploy-api-docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | build-and-deploy:
9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main
10 | secrets: inherit
11 | with:
12 | package_name: mysql-kit
13 | modules: MySQLKit
14 | pathsToInvalidate: /mysqlkit/*
15 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | concurrency:
3 | group: ${{ github.workflow }}-${{ github.ref }}
4 | cancel-in-progress: true
5 | on:
6 | pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
7 | push: { branches: [ main ] }
8 |
9 | env:
10 | LOG_LEVEL: info
11 | MYSQL_HOSTNAME: 'mysql-a'
12 | MYSQL_HOSTNAME_A: 'mysql-a'
13 | MYSQL_HOSTNAME_B: 'mysql-b'
14 | MYSQL_DATABASE: 'test_database'
15 | MYSQL_DATABASE_A: 'test_database'
16 | MYSQL_DATABASE_B: 'test_database'
17 | MYSQL_USERNAME: 'test_username'
18 | MYSQL_USERNAME_A: 'test_username'
19 | MYSQL_USERNAME_B: 'test_username'
20 | MYSQL_PASSWORD: 'test_password'
21 | MYSQL_PASSWORD_A: 'test_password'
22 | MYSQL_PASSWORD_B: 'test_password'
23 |
24 | jobs:
25 | api-breakage:
26 | if: ${{ github.event_name == 'pull_request' && !(github.event.pull_request.draft || false) }}
27 | runs-on: ubuntu-latest
28 | container: swift:noble
29 | steps:
30 | - name: Checkout
31 | uses: actions/checkout@v4
32 | with: { 'fetch-depth': 0 }
33 | - name: API breaking changes
34 | run: |
35 | git config --global --add safe.directory "${GITHUB_WORKSPACE}"
36 | swift package diagnose-api-breaking-changes origin/main
37 |
38 | dependents:
39 | if: ${{ !(github.event.pull_request.draft || false) }}
40 | runs-on: ubuntu-latest
41 | services:
42 | mysql-a:
43 | image: ${{ matrix.dbimage }}
44 | env:
45 | MYSQL_ALLOW_EMPTY_PASSWORD: "true"
46 | MYSQL_USER: test_username
47 | MYSQL_PASSWORD: test_password
48 | MYSQL_DATABASE: test_database
49 | mysql-b:
50 | image: ${{ matrix.dbimage }}
51 | env:
52 | MYSQL_ALLOW_EMPTY_PASSWORD: "true"
53 | MYSQL_USER: test_username
54 | MYSQL_PASSWORD: test_password
55 | MYSQL_DATABASE: test_database
56 | container: swift:6.1-noble
57 | strategy:
58 | fail-fast: false
59 | matrix:
60 | dbimage:
61 | - mysql:5.7
62 | - mysql:9
63 | - mariadb:11
64 | - percona:8.0
65 | steps:
66 | - name: Check out package
67 | uses: actions/checkout@v4
68 | with: { path: 'mysql-kit' }
69 | - name: Check out dependent
70 | uses: actions/checkout@v4
71 | with:
72 | repository: vapor/fluent-mysql-driver
73 | path: fluent-mysql-driver
74 | - name: Use local package
75 | run: swift package --package-path fluent-mysql-driver edit mysql-kit --path ./mysql-kit
76 | - name: Run tests with Thread Sanitizer
77 | run: swift test --package-path fluent-mysql-driver --sanitize=thread
78 |
79 | # Run unit tests (Linux), do code coverage in same job to cut down on extra builds
80 | linux-unit:
81 | if: ${{ !(github.event.pull_request.draft || false) }}
82 | strategy:
83 | fail-fast: false
84 | matrix:
85 | dbimage:
86 | - mysql:5.7
87 | - mysql:8.0
88 | - mysql:9.3
89 | - mariadb:10.4
90 | - mariadb:11
91 | - percona:8.0
92 | runner:
93 | # List is deliberately incomplete; we want to avoid running 50 jobs on every commit
94 | - swift:5.10-jammy
95 | - swift:6.0-noble
96 | - swift:6.1-noble
97 | container: ${{ matrix.runner }}
98 | runs-on: ubuntu-latest
99 | services:
100 | mysql-a:
101 | image: ${{ matrix.dbimage }}
102 | env:
103 | MYSQL_ALLOW_EMPTY_PASSWORD: "true"
104 | MYSQL_USER: test_username
105 | MYSQL_PASSWORD: test_password
106 | MYSQL_DATABASE: test_database
107 | steps:
108 | - name: Check out package
109 | uses: actions/checkout@v4
110 | - name: Run local tests with coverage and TSan
111 | run: swift test --enable-code-coverage --sanitize=thread
112 | - name: Submit coverage report to Codecov.io
113 | uses: vapor/swift-codecov-action@v0.3
114 | with:
115 | codecov_token: ${{ secrets.CODECOV_TOKEN }}
116 |
117 | # Run unit tests (macOS). Don't bother with lots of variations, Linux will cover that.
118 | macos-unit:
119 | if: ${{ !(github.event.pull_request.draft || false) }}
120 | strategy:
121 | fail-fast: false
122 | matrix:
123 | include:
124 | - macos-version: macos-14
125 | xcode-version: latest-stable
126 | - macos-version: macos-15
127 | xcode-version: latest-stable
128 | runs-on: ${{ matrix.macos-version }}
129 | steps:
130 | - name: Select latest available Xcode
131 | uses: maxim-lobanov/setup-xcode@v1
132 | with:
133 | xcode-version: ${{ matrix.xcode-version }}
134 | - name: Install MySQL server from Homebrew
135 | run: brew install mysql && brew link --force mysql
136 | - name: Start MySQL server
137 | run: brew services start mysql
138 | - name: Wait for MySQL server to be ready
139 | run: until echo | mysql -uroot; do sleep 1; done
140 | timeout-minutes: 5
141 | - name: Set up MySQL databases and privileges
142 | run: |
143 | mysql -uroot --batch <<-'SQL'
144 | CREATE USER test_username@localhost IDENTIFIED BY 'test_password';
145 | CREATE DATABASE test_database;
146 | GRANT ALL PRIVILEGES ON test_database.* TO test_username@localhost;
147 | SQL
148 | - name: Check out code
149 | uses: actions/checkout@v4
150 | - name: Run tests with code coverage
151 | run: swift test --enable-code-coverage
152 | env: { MYSQL_HOSTNAME: '127.0.0.1' }
153 | - name: Submit coverage report to Codecov.io
154 | uses: vapor/swift-codecov-action@v0.3
155 | with:
156 | codecov_token: ${{ secrets.CODECOV_TOKEN }}
157 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .build
2 | Packages
3 | *.xcodeproj
4 | Package.pins
5 | Package.resolved
6 | .DS_Store
7 | DerivedData
8 | .swiftpm
9 |
10 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | version: 1
2 | metadata:
3 | authors: "Maintained by the Vapor Core Team with hundreds of contributions from the Vapor Community."
4 | external_links:
5 | documentation: "https://api.vapor.codes/postgreskit/documentation/postgreskit/"
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Qutheory, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.10
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "mysql-kit",
6 | platforms: [
7 | .macOS(.v10_15),
8 | .iOS(.v13),
9 | .watchOS(.v6),
10 | .tvOS(.v13),
11 | ],
12 | products: [
13 | .library(name: "MySQLKit", targets: ["MySQLKit"]),
14 | ],
15 | dependencies: [
16 | .package(url: "https://github.com/vapor/async-kit.git", from: "1.20.0"),
17 | .package(url: "https://github.com/vapor/mysql-nio.git", from: "1.7.2"),
18 | .package(url: "https://github.com/vapor/sql-kit.git", from: "3.33.0"),
19 | .package(url: "https://github.com/apple/swift-crypto.git", "2.0.0" ..< "4.0.0"),
20 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.82.0"),
21 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.30.0"),
22 | ],
23 | targets: [
24 | .target(
25 | name: "MySQLKit",
26 | dependencies: [
27 | .product(name: "AsyncKit", package: "async-kit"),
28 | .product(name: "MySQLNIO", package: "mysql-nio"),
29 | .product(name: "SQLKit", package: "sql-kit"),
30 | .product(name: "Crypto", package: "swift-crypto"),
31 | .product(name: "NIOFoundationCompat", package: "swift-nio"),
32 | .product(name: "NIOSSL", package: "swift-nio-ssl"),
33 | ],
34 | swiftSettings: swiftSettings
35 | ),
36 | .testTarget(
37 | name: "MySQLKitTests",
38 | dependencies: [
39 | .product(name: "SQLKitBenchmark", package: "sql-kit"),
40 | .target(name: "MySQLKit"),
41 | ],
42 | swiftSettings: swiftSettings
43 | ),
44 | ]
45 | )
46 |
47 | var swiftSettings: [SwiftSetting] { [
48 | .enableUpcomingFeature("ExistentialAny"),
49 | .enableUpcomingFeature("ConciseMagicFile"),
50 | .enableUpcomingFeature("ForwardTrailingClosures"),
51 | .enableUpcomingFeature("DisableOutwardActorInference"),
52 | .enableUpcomingFeature("MemberImportVisibility"),
53 | .enableExperimentalFeature("StrictConcurrency=complete"),
54 | ] }
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | MySQLKit is an [SQLKit] driver for MySQL clients. It supports building and serializing MySQL-dialect SQL queries. MySQLKit uses [MySQLNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling.
16 |
17 | [SQLKit]: https://github.com/vapor/sql-kit
18 | [MySQLNIO]: https://github.com/vapor/mysql-nio
19 | [AsyncKit]: https://github.com/vapor/async-kit
20 |
21 | ### Usage
22 |
23 | Use the SPM string to easily include the dependendency in your `Package.swift` file.
24 |
25 | ```swift
26 | .package(url: "https://github.com/vapor/mysql-kit.git", from: "4.0.0")
27 | ```
28 |
29 | ### Supported Platforms
30 |
31 | MySQLKit supports the following platforms:
32 |
33 | - Ubuntu 20.04+
34 | - macOS 10.15+
35 |
36 | ### Configuration
37 |
38 | Database connection options and credentials are specified using a `MySQLConfiguration` struct.
39 |
40 | ```swift
41 | import MySQLKit
42 |
43 | let configuration = MySQLConfiguration(
44 | hostname: "localhost",
45 | port: 3306,
46 | username: "vapor_username",
47 | password: "vapor_password",
48 | database: "vapor_database"
49 | )
50 | ```
51 |
52 | URL string based configuration is also supported.
53 |
54 | ```swift
55 | guard let configuration = MySQLConfiguration(url: "mysql://...") else {
56 | ...
57 | }
58 | ```
59 |
60 | To connect via unix-domain sockets, use `unixDomainSocketPath` instead of `hostname` and `port`.
61 |
62 | ```swift
63 | let configuration = MySQLConfiguration(
64 | unixDomainSocketPath: "/path/to/socket",
65 | username: "vapor_username",
66 | password: "vapor_password",
67 | database: "vapor_database"
68 | )
69 | ```
70 |
71 | ### Connection Pool
72 |
73 | Once you have a `MySQLConfiguration`, you can use it to create a connection source and pool.
74 |
75 | ```swift
76 | let eventLoopGroup: EventLoopGroup = ...
77 | defer { try! eventLoopGroup.syncShutdown() }
78 |
79 | let pools = EventLoopGroupConnectionPool(
80 | source: MySQLConnectionSource(configuration: configuration),
81 | on: eventLoopGroup
82 | )
83 | defer { pools.shutdown() }
84 | ```
85 |
86 | First create a `MySQLConnectionSource` using the configuration struct. This type is responsible for creating new connections to your database server as needed.
87 |
88 | Next, use the connection source to create an `EventLoopGroupConnectionPool`. You will also need to pass an `EventLoopGroup`. For more information on creating an `EventLoopGroup`, visit SwiftNIO's [documentation](https://apple.github.io/swift-nio/docs/current/NIO/index.html). Make sure to shutdown the connection pool before it deinitializes.
89 |
90 | `EventLoopGroupConnectionPool` is a collection of pools for each event loop. When using `EventLoopGroupConnectionPool` directly, random event loops will be chosen as needed.
91 |
92 | ```swift
93 | pools.withConnection { conn
94 | print(conn) // MySQLConnection on randomly chosen event loop
95 | }
96 | ```
97 |
98 | To get a pool for a specific event loop, use `pool(for:)`. This returns an `EventLoopConnectionPool`.
99 |
100 | ```swift
101 | let eventLoop: EventLoop = ...
102 | let pool = pools.pool(for: eventLoop)
103 |
104 | pool.withConnection { conn
105 | print(conn) // MySQLConnection on eventLoop
106 | }
107 | ```
108 |
109 | ### MySQLDatabase
110 |
111 | Both `EventLoopGroupConnectionPool` and `EventLoopConnectionPool` can be used to create instances of `MySQLDatabase`.
112 |
113 | ```swift
114 | let mysql = pool.database(logger: ...) // MySQLDatabase
115 | let rows = try mysql.simpleQuery("SELECT @@version;").wait()
116 | ```
117 |
118 | Visit [MySQLNIO's docs](https://github.com/vapor/mysql-nio) for more information on using `MySQLDatabase`.
119 |
120 | ### SQLDatabase
121 |
122 | A `MySQLDatabase` can be used to create an instance of `SQLDatabase`.
123 |
124 | ```swift
125 | let sql = mysql.sql() // SQLDatabase
126 | let planets = try sql.select().column("*").from("planets").all().wait()
127 | ```
128 |
129 | Visit [SQLKit's docs](https://api.vapor.codes/sqlkit/documentation/sqlkit) for more information on using `SQLDatabase`.
130 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/ConnectionPool+MySQL.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import Logging
3 | import MySQLNIO
4 |
5 | extension EventLoopGroupConnectionPool where Source == MySQLConnectionSource {
6 | /// Return a `MySQLDatabase` which rotates between the available connection pools.
7 | ///
8 | /// - Parameter logger: The logger to assign to the database.
9 | /// - Returns: A `MySQLDatabase`.
10 | public func database(logger: Logger) -> any MySQLDatabase {
11 | EventLoopGroupConnectionPoolMySQLDatabase(pools: self, logger: logger)
12 | }
13 | }
14 |
15 | extension EventLoopConnectionPool where Source == MySQLConnectionSource {
16 | /// Return a `MySQLDatabase` from this connection pool.
17 | ///
18 | /// - Parameter logger: The logger to assign to the database.
19 | /// - Returns: A `MySQLDatabase`.
20 | public func database(logger: Logger) -> any MySQLDatabase {
21 | EventLoopConnectionPoolMySQLDatabase(pool: self, logger: logger)
22 | }
23 | }
24 |
25 | private struct EventLoopGroupConnectionPoolMySQLDatabase: MySQLDatabase {
26 | let pools: EventLoopGroupConnectionPool
27 | let logger: Logger
28 |
29 | // See `MySQLDatabase.eventLoop`.
30 | var eventLoop: any EventLoop {
31 | self.pools.eventLoopGroup.any()
32 | }
33 |
34 | // See `MySQLDatabase.send(_:logger:)`.
35 | func send(_ command: any MySQLCommand, logger: Logger) -> EventLoopFuture {
36 | self.pools.withConnection(logger: logger) {
37 | $0.send(command, logger: logger)
38 | }
39 | }
40 |
41 | // See `MySQLDatabase.withConnection(_:)`.
42 | func withConnection(_ closure: @escaping (MySQLConnection) -> EventLoopFuture) -> EventLoopFuture {
43 | self.pools.withConnection(logger: self.logger, closure)
44 | }
45 | }
46 |
47 | private struct EventLoopConnectionPoolMySQLDatabase: MySQLDatabase {
48 | let pool: EventLoopConnectionPool
49 | let logger: Logger
50 |
51 | // See `MySQLDatabase.eventLoop`.
52 | var eventLoop: any EventLoop {
53 | self.pool.eventLoop
54 | }
55 |
56 | // See `MySQLDatabase.send(_:logger:)`.
57 | func send(_ command: any MySQLCommand, logger: Logger) -> EventLoopFuture {
58 | self.pool.withConnection(logger: logger) {
59 | $0.send(command, logger: logger)
60 | }
61 | }
62 |
63 | // See `MySQLDatabase.withConnection(_:)`.
64 | func withConnection(_ closure: @escaping (MySQLConnection) -> EventLoopFuture) -> EventLoopFuture {
65 | self.pool.withConnection(logger: self.logger, closure)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/Docs.docc/MySQLKit.md:
--------------------------------------------------------------------------------
1 | # ``MySQLKit``
2 |
3 | @Metadata {
4 | @TitleHeading(Package)
5 | }
6 |
7 | MySQLKit is a library providing an SQLKit driver for MySQLNIO.
8 |
9 | ## Overview
10 |
11 | This package provides the "foundational" level of support for using [Fluent] with MySQL by implementing the requirements of an [SQLKit] driver. It is responsible for:
12 |
13 | - Managing the underlying MySQL library ([MySQLNIO]),
14 | - Providing a two-way bridge between MySQLNIO and SQLKit's generic data and metadata formats, and
15 | - Presenting an interface for establishing, managing, and interacting with database connections via [AsyncKit].
16 |
17 | > Tip: A FluentKit driver for MySQL is provided by the [FluentMySQLDriver] package.
18 |
19 | ## Version Support
20 |
21 | This package uses [MySQLNIO] for all underlying database interactions. It is compatible with all versions of MySQL and all platforms supported by that package.
22 |
23 | > Caution: There is one exception to the above at the time of this writing: This package requires Swift 5.8 or newer, whereas MySQLNIO continues to support Swift 5.6.
24 |
25 | [SQLKit]: https://swiftpackageindex.com/vapor/sql-kit
26 | [MySQLNIO]: https://swiftpackageindex.com/vapor/mysql-nio
27 | [Fluent]: https://swiftpackageindex.com/vapor/fluent-kit
28 | [FluentMySQLDriver]: https://swiftpackageindex.com/vapor/fluent-mysql-driver
29 | [AsyncKit]: https://swiftpackageindex.com/vapor/async-kit
30 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/Docs.docc/Resources/vapor-mysqlkit-logo.svg:
--------------------------------------------------------------------------------
1 |
45 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/Docs.docc/theme-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "theme": {
3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
4 | "border-radius": "0",
5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
7 | "color": {
8 | "mysql-turquoise": "#02758f",
9 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-mysql-turquoise) 30%, #000 100%)",
10 | "documentation-intro-accent": "var(--color-mysql-turquoise)",
11 | "documentation-intro-eyebrow": "white",
12 | "documentation-intro-figure": "white",
13 | "documentation-intro-title": "white",
14 | "logo-base": { "dark": "#fff", "light": "#000" },
15 | "logo-shape": { "dark": "#000", "light": "#fff" },
16 | "fill": { "dark": "#000", "light": "#fff" }
17 | },
18 | "icons": { "technology": "/mysqlkit/images/MySQLKit/vapor-mysqlkit-logo.svg" }
19 | },
20 | "features": {
21 | "quickNavigation": { "enable": true },
22 | "i18n": { "enable": true }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/Exports.swift:
--------------------------------------------------------------------------------
1 | @_documentation(visibility: internal) @_exported import MySQLNIO
2 | @_documentation(visibility: internal) @_exported import AsyncKit
3 | @_documentation(visibility: internal) @_exported import SQLKit
4 | @_documentation(visibility: internal) @_exported import struct Foundation.URL
5 | @_documentation(visibility: internal) @_exported import struct Foundation.Data
6 | @_documentation(visibility: internal) @_exported import struct NIOSSL.TLSConfiguration
7 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLConfiguration.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NIOSSL
3 | import NIOCore
4 | import NIOPosix // for inet_pton()
5 | import NIOConcurrencyHelpers
6 |
7 | /// A set of parameters used to connect to a MySQL database.
8 | public struct MySQLConfiguration: Sendable {
9 | /// Underlying storage for ``address``.
10 | private var _address: @Sendable () throws -> SocketAddress
11 | /// Underlying storage for ``username``.
12 | private var _username: String
13 | /// Underlying storage for ``password``.
14 | private var _password: String
15 | /// Underlying storage for ``database``.
16 | private var _database: String?
17 | /// Underlying storage for ``tlsConfiguration``.
18 | private var _tlsConfiguration: TLSConfiguration?
19 | /// Underlying storage for ``_hostname``.
20 | private var _internal_hostname: String?
21 |
22 | /// Lock for access to underlying storage.
23 | private let lock: NIOLock = .init()
24 |
25 | /// A closure which returns the NIO `SocketAddress` for a server.
26 | public var address: @Sendable () throws -> SocketAddress {
27 | get { self.lock.withLock { self._address } }
28 | set { self.lock.withLock { self._address = newValue } }
29 | }
30 |
31 | /// The username used to authenticate the connection.
32 | public var username: String {
33 | get { self.lock.withLock { self._username } }
34 | set { self.lock.withLock { self._username = newValue } }
35 | }
36 |
37 | /// The password used to authenticate the connection. May be an empty string.
38 | public var password: String {
39 | get { self.lock.withLock { self._password } }
40 | set { self.lock.withLock { self._password = newValue } }
41 | }
42 |
43 | /// An optional initial default database for the connection.
44 | public var database: String? {
45 | get { self.lock.withLock { self._database } }
46 | set { self.lock.withLock { self._database = newValue } }
47 | }
48 |
49 | /// Optional configuration for TLS-based connection encryption.
50 | public var tlsConfiguration: TLSConfiguration? {
51 | get { self.lock.withLock { self._tlsConfiguration } }
52 | set { self.lock.withLock { self._tlsConfiguration = newValue } }
53 | }
54 |
55 | /// The IANA-assigned port number for MySQL (3306).
56 | ///
57 | /// This is the default port used by MySQL servers. Equivalent to
58 | /// `UInt16(getservbyname("mysql", "tcp").pointee.s_port).byteSwapped`.
59 | public static var ianaPortNumber: Int { 3306 }
60 |
61 | var _hostname: String? {
62 | get { self.lock.withLock { self._internal_hostname } }
63 | set { self.lock.withLock { self._internal_hostname = newValue } }
64 | }
65 |
66 | /// Create a ``MySQLConfiguration`` from an appropriately-formatted URL string.
67 | ///
68 | /// See ``MySQLConfiguration/init(url:)-4mmel`` for details of the accepted URL format.
69 | ///
70 | /// - Parameter url: A URL-formatted MySQL connection string. See ``init(url:)-4mmel`` for syntax details.
71 | /// - Returns: `nil` if `url` is not a valid RFC 3986 URI with an authority component (e.g. a URL).
72 | public init?(url: String) {
73 | guard let url = URL(string: url) else {
74 | return nil
75 | }
76 | self.init(url: url)
77 | }
78 |
79 | /// Create a ``MySQLConfiguration`` from an appropriately-formatted `URL`.
80 | ///
81 | /// The supported URL formats are:
82 | ///
83 | /// mysql://username:password@hostname:port/database?ssl-mode=mode
84 | /// mysql+tcp://username:password@hostname:port/database?ssl-mode=mode
85 | /// mysql+uds://username:password@localhost/path?ssl-mode=mode#database
86 | ///
87 | /// The `mysql+tcp` scheme requests a connection over TCP. The `mysql` scheme is an alias
88 | /// for `mysql+tcp`. Only the `hostname` and `username` components are required.
89 | ///
90 | /// The ` mysql+uds` scheme requests a connection via a UNIX domain socket. The `username` and
91 | /// `path` components are required. The authority must always be empty or `localhost`, and may not
92 | /// specify a port.
93 | ///
94 | /// The allowed `mode` values for `ssl-mode` are:
95 | ///
96 | /// Value|Behavior
97 | /// -|-
98 | /// `DISABLED`|Don't use TLS, even if the server supports it.
99 | /// `PREFERRED`|Use TLS if possible.
100 | /// `REQUIRED`|Enforce TLS support, including CA and hostname verification.
101 | ///
102 | /// `ssl-mode` values are case-insensitive. `VERIFY_CA` and `VERIFY_IDENTITY` are accepted as aliases
103 | /// of `REQUIRED`. `tls-mode`, `tls`, and `ssl` are recognized aliases of `ssl-mode`.
104 | ///
105 | /// If no `ssl-mode` is specified, the default mode is `REQUIRED` for TCP connections, or `DISABLED`
106 | /// for UDS connections. If more than one mode is specified, the last one wins. Whenever a TLS
107 | /// connection is made, full certificate verification (both chain of trust and hostname match)
108 | /// is always enforced, regardless of the mode used.
109 | ///
110 | /// > Warning: At this time of this writing, `PREFERRED` is the same as `REQUIRED`, due to
111 | /// > limitations of the underlying implementation. A future version will remove this restriction.
112 | ///
113 | /// > Note: It is possible to emulate `libmysqlclient`'s definitions for `REQUIRED` (TLS enforced, but
114 | /// > without certificate verification) and `VERIFY_CA` (TLS enforced with no hostname verification) by
115 | /// > manually specifying the TLS configuration instead of using a URL. It is _strongly_ recommended for
116 | /// > both security and privacy reasons to always leave full certificate verification enabled whenever
117 | /// > possible. See NIOSSL's [`TLSConfiguration`](tlsconfig) for additional information and recommendations.
118 | ///
119 | /// [tlsconfig]:
120 | /// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/tlsconfiguration
121 | ///
122 | /// - Parameter url: A `URL` containing MySQL connection parameters.
123 | /// - Returns: `nil` if `url` is missing required information or has an invalid scheme.
124 | public init?(url: URL) {
125 | guard let comp = URLComponents(url: url, resolvingAgainstBaseURL: true), let username = comp.user else {
126 | return nil
127 | }
128 |
129 | func decideTLSConfig(from queryItems: [URLQueryItem], defaultMode: String) -> TLSConfiguration?? {
130 | switch queryItems.last(where: { ["ssl-mode", "tls-mode", "tls", "ssl"].contains($0.name.lowercased()) })?.value ?? defaultMode {
131 | case "REQUIRED", "VERIFY_CA", "VERIFY_IDENTITY", "PREFERRED": return .some(.some(.makeClientConfiguration()))
132 | case "DISABLED": return .some(.none)
133 | default: return .none
134 | }
135 | }
136 |
137 | switch comp.scheme {
138 | case "mysql", "mysql+tcp":
139 | guard let hostname = comp.host, !hostname.isEmpty else {
140 | return nil
141 | }
142 | guard case .some(let tlsConfig) = decideTLSConfig(from: comp.queryItems ?? [], defaultMode: "REQUIRED") else {
143 | return nil
144 | }
145 | self.init(
146 | hostname: hostname, port: comp.port ?? Self.ianaPortNumber,
147 | username: username, password: comp.password ?? "",
148 | database: url.lastPathComponent.isEmpty ? nil : url.lastPathComponent, tlsConfiguration: tlsConfig
149 | )
150 | case "mysql+uds":
151 | guard (comp.host?.isEmpty ?? true || comp.host == "localhost"), comp.port == nil, !comp.path.isEmpty, comp.path != "/" else {
152 | return nil
153 | }
154 | guard case .some(let tlsConfig) = decideTLSConfig(from: comp.queryItems ?? [], defaultMode: "DISABLED") else {
155 | return nil
156 | }
157 | self.init(
158 | unixDomainSocketPath: comp.path,
159 | username: username, password: comp.password ?? "",
160 | database: comp.fragment, tlsConfiguration: tlsConfig
161 | )
162 | default:
163 | return nil
164 | }
165 | }
166 |
167 | /// Create a ``MySQLConfiguration`` for connecting to a server through a UNIX domain socket.
168 | ///
169 | /// - Parameters:
170 | /// - unixDomainSocketPath: The path to the UNIX domain socket to connect through.
171 | /// - username: The username to use for the connection.
172 | /// - password: The password (empty string for none) to use for the connection.
173 | /// - database: The default database for the connection, if any.
174 | public init(
175 | unixDomainSocketPath: String,
176 | username: String,
177 | password: String,
178 | database: String? = nil
179 | ) {
180 | self.init(
181 | unixDomainSocketPath: unixDomainSocketPath,
182 | username: username,
183 | password: password,
184 | database: database,
185 | tlsConfiguration: nil
186 | )
187 | }
188 |
189 | /// Create a ``MySQLConfiguration`` for connecting to a server through a UNIX domain socket.
190 | ///
191 | /// - Parameters:
192 | /// - unixDomainSocketPath: The path to the UNIX domain socket to connect through.
193 | /// - username: The username to use for the connection.
194 | /// - password: The password (empty string for none) to use for the connection.
195 | /// - database: The default database for the connection, if any.
196 | /// - tlsConfiguration: An optional `TLSConfiguration` specifying encryption for the connection.
197 | public init(
198 | unixDomainSocketPath: String,
199 | username: String,
200 | password: String,
201 | database: String? = nil,
202 | tlsConfiguration: TLSConfiguration?
203 | ) {
204 | self._address = {
205 | try SocketAddress.init(unixDomainSocketPath: unixDomainSocketPath)
206 | }
207 | self._username = username
208 | self._password = password
209 | self._database = database
210 | self._tlsConfiguration = tlsConfiguration
211 | self._internal_hostname = nil
212 | }
213 |
214 | /// Create a ``MySQLConfiguration`` for connecting to a server with a hostname and optional port.
215 | ///
216 | /// - Parameters:
217 | /// - hostname: The hostname to connect to.
218 | /// - port: A TCP port number to connect on. Defaults to the IANA-assigned MySQL port number (3306).
219 | /// - username: The username to use for the connection.
220 | /// - password: The pasword (empty string for none) to use for the connection.
221 | /// - database: The default database fr the connection, if any.
222 | /// - tlsConfiguration: An optional `TLSConfiguration` specifying encryption for the connection.
223 | public init(
224 | hostname: String,
225 | port: Int = Self.ianaPortNumber,
226 | username: String,
227 | password: String,
228 | database: String? = nil,
229 | tlsConfiguration: TLSConfiguration? = .makeClientConfiguration()
230 | ) {
231 | self._address = {
232 | try SocketAddress.makeAddressResolvingHost(hostname, port: port)
233 | }
234 | self._username = username
235 | self._database = database
236 | self._password = password
237 | if let tlsConfiguration = tlsConfiguration {
238 | self._tlsConfiguration = tlsConfiguration
239 |
240 | // Temporary fix - this logic should be removed once MySQLNIO is updated
241 | var n4 = in_addr(), n6 = in6_addr()
242 | if inet_pton(AF_INET, hostname, &n4) != 1 && inet_pton(AF_INET6, hostname, &n6) != 1 {
243 | self._internal_hostname = hostname
244 | }
245 | }
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLConnectionSource.swift:
--------------------------------------------------------------------------------
1 | import NIOCore
2 | import Logging
3 | import MySQLNIO
4 | import AsyncKit
5 |
6 | /// A `ConnectionPoolSource` providing MySQL database connections for a given ``MySQLConfiguration``.
7 | public struct MySQLConnectionSource: ConnectionPoolSource {
8 | /// A ``MySQLConfiguration`` used to create connections.
9 | public let configuration: MySQLConfiguration
10 |
11 | /// Create a ``MySQLConnectionSource``.
12 | ///
13 | /// - Parameter configuration: The configuration for new connections.
14 | public init(configuration: MySQLConfiguration) {
15 | self.configuration = configuration
16 | }
17 |
18 | // See `ConnectionPoolSource.makeConnection(logger:on:)`.
19 | public func makeConnection(logger: Logger, on eventLoop: any EventLoop) -> EventLoopFuture {
20 | let address: SocketAddress
21 |
22 | do {
23 | address = try self.configuration.address()
24 | } catch {
25 | return eventLoop.makeFailedFuture(error)
26 | }
27 | return MySQLConnection.connect(
28 | to: address,
29 | username: self.configuration.username,
30 | database: self.configuration.database ?? self.configuration.username,
31 | password: self.configuration.password,
32 | tlsConfiguration: self.configuration.tlsConfiguration,
33 | serverHostname: self.configuration._hostname,
34 | logger: logger,
35 | on: eventLoop
36 | )
37 | }
38 | }
39 |
40 | extension MySQLNIO.MySQLConnection: AsyncKit.ConnectionPoolItem {} // Fully qualifying the type names implies @retroactive
41 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLDataDecoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import MySQLNIO
3 | import NIOFoundationCompat
4 | @_spi(CodableUtilities) import SQLKit
5 |
6 | /// Translates `MySQLData` values received from the database into `Decodable` values.
7 | ///
8 | /// Types which conform to `MySQLDataConvertible` are converted directly to the requested type. For other types,
9 | /// an attempt is made to interpret the database value as JSON and decode the type from it.
10 | public struct MySQLDataDecoder: Sendable {
11 | /// A wrapper to silence `Sendable` warnings for `JSONDecoder` when not on macOS.
12 | struct FakeSendable: @unchecked Sendable { let value: T }
13 |
14 | /// The `JSONDecoder` used for decoding values that can't be directly converted.
15 | let json: FakeSendable
16 |
17 | /// Initialize a ``MySQLDataDecoder`` with a JSON decoder.
18 | ///
19 | /// - Parameter json: A `JSONDecoder` to use for decoding types that can't be directly converted. Defaults
20 | /// to an unconfigured decoder.
21 | public init(json: JSONDecoder = .init()) {
22 | self.json = .init(value: json)
23 | }
24 |
25 | /// Convert the given `MySQLData` into a value of type `T`, if possible.
26 | ///
27 | /// - Parameters:
28 | /// - type: The desired result type.
29 | /// - data: The data to decode.
30 | /// - Returns: The decoded value, if successful.
31 | public func decode(_ type: T.Type, from data: MySQLData) throws -> T {
32 | // If `T` can be converted directly, just do so.
33 | if let convertible = T.self as? any MySQLDataConvertible.Type {
34 | guard let value = convertible.init(mysqlData: data) else {
35 | throw DecodingError.typeMismatch(T.self, .init(
36 | codingPath: [],
37 | debugDescription: "Could not convert MySQL data to \(T.self): \(data)"
38 | ))
39 | }
40 | return value as! T
41 | } else {
42 | // Probably either a JSON array/object or an enum type not using @Enum. See if it can be "unwrapped" as a
43 | // single-value decoding container, since this is much faster than attempting a JSON decode; this will
44 | // handle "box" types such as `RawRepresentable` enums or fall back on JSON decoding if necessary.
45 | do {
46 | return try T.init(from: NestedSingleValueUnwrappingDecoder(decoder: self, data: data))
47 | } catch is SQLCodingError where [.blob, .json, .longBlob, .mediumBlob, .string, .tinyBlob, .varString, .varchar].contains(data.type) {
48 | // Couldn't unwrap it, but it's textual. Try decoding as JSON as a last ditch effort.
49 | return try self.json.value.decode(T.self, from: data.buffer ?? .init())
50 | }
51 | }
52 | }
53 |
54 | /// A trivial decoder for unwrapping types which decode as trivial single-value containers. This allows for
55 | /// correct handling of types such as `Optional` when they do not conform to `MySQLDataCovnertible`.
56 | private final class NestedSingleValueUnwrappingDecoder: Decoder, SingleValueDecodingContainer {
57 | // See `Decoder.codingPath` and `SingleValueDecodingContainer.codingPath`.
58 | var codingPath: [any CodingKey] { [] }
59 |
60 | // See `Decoder.userInfo`.
61 | var userInfo: [CodingUserInfoKey: Any] { [:] }
62 |
63 | /// The parent ``MySQLDataDecoder``.
64 | let dataDecoder: MySQLDataDecoder
65 |
66 | /// The data to decode.
67 | let data: MySQLData
68 |
69 | /// Create a new decoder with a ``MySQLDataDecoder`` and the data to decode.
70 | init(decoder: MySQLDataDecoder, data: MySQLData) {
71 | self.dataDecoder = decoder
72 | self.data = data
73 | }
74 |
75 | // See `Decoder.container(keyedBy:)`.
76 | func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer {
77 | throw .invalid(at: self.codingPath)
78 | }
79 |
80 | // See `Decoder.unkeyedContainer()`.
81 | func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
82 | throw .invalid(at: self.codingPath)
83 | }
84 |
85 | // See `Decoder.singleValueContainer()`.
86 | func singleValueContainer() throws -> any SingleValueDecodingContainer {
87 | self
88 | }
89 |
90 | // See `SingleValueDecodingContainer.decodeNil()`.
91 | func decodeNil() -> Bool {
92 | self.data.type == .null || self.data.buffer == nil
93 | }
94 |
95 | // See `SingleValueDecodingContainer.decode(_:)`.
96 | func decode(_: T.Type) throws -> T {
97 | try self.dataDecoder.decode(T.self, from: self.data)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLDataEncoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import MySQLNIO
3 | import NIOFoundationCompat
4 | @_spi(CodableUtilities) import SQLKit
5 |
6 | /// Translates `Encodable` values into `MySQLData` values suitable for use with a `MySQLDatabase`.
7 | ///
8 | /// Types which conform to `MySQLDataConvertible` are converted directly to `MySQLData`. Other types are
9 | /// encoded as JSON and sent to the database as text.
10 | public struct MySQLDataEncoder: Sendable {
11 | /// A wrapper to silence `Sendable` warnings for `JSONEncoder` when not on macOS.
12 | struct FakeSendable: @unchecked Sendable { let value: T }
13 |
14 | /// The `JSONEncoder` used for encoding values that can't be directly converted.
15 | let json: FakeSendable
16 |
17 | /// Initialize a ``MySQLDataEncoder`` with a JSON encoder.
18 | ///
19 | /// - Parameter json: A `JSONEncoder` to use for encoding types that can't be directly converted. Defaults
20 | /// to an unconfigured encoder.
21 | public init(json: JSONEncoder = .init()) {
22 | self.json = .init(value: json)
23 | }
24 |
25 | /// Convert the given `Encodable` value to a `MySQLData` value, if possible.
26 | ///
27 | /// - Parameter value: The value to convert.
28 | /// - Returns: A converted `MySQLData` value, if successful.
29 | public func encode(_ value: any Encodable) throws -> MySQLData {
30 | if let custom = value as? any MySQLDataConvertible {
31 | guard let data = custom.mysqlData else {
32 | throw EncodingError.invalidValue(value, .init(codingPath: [], debugDescription: "Couldn't get MySQL encoding from value '\(value)'"))
33 | }
34 | return data
35 | } else {
36 | do {
37 | let encoder = NestedSingleValueUnwrappingEncoder(dataEncoder: self)
38 |
39 | try value.encode(to: encoder)
40 | guard let value = encoder.value else {
41 | throw SQLCodingError.unsupportedOperation("missing value", codingPath: [])
42 | }
43 | return value
44 | } catch is SQLCodingError {
45 | return MySQLData(
46 | type: .string,
47 | format: .text,
48 | buffer: try self.json.value.encodeAsByteBuffer(value, allocator: .init()),
49 | isUnsigned: true
50 | )
51 | }
52 | }
53 | }
54 |
55 | /// A trivial encoder for unwrapping types which encode as trivial single-value containers. This allows for
56 | /// correct handling of types such as `Optional` when they do not conform to `MySQLDataCovnertible`.
57 | private final class NestedSingleValueUnwrappingEncoder: Encoder, SingleValueEncodingContainer {
58 | // See `Encoder.userInfo`.
59 | var userInfo: [CodingUserInfoKey: Any] { [:] }
60 |
61 | // See `Encoder.codingPath` and `SingleValueEncodingContainer.codingPath`.
62 | var codingPath: [any CodingKey] { [] }
63 |
64 | /// The parent ``MySQLDataEncoder``.
65 | let dataEncoder: MySQLDataEncoder
66 |
67 | /// Storage for the resulting converted value.
68 | var value: MySQLData? = nil
69 |
70 | /// Create a new encoder with a ``MySQLDataEncoder``.
71 | init(dataEncoder: MySQLDataEncoder) {
72 | self.dataEncoder = dataEncoder
73 | }
74 |
75 | // See `Encoder.container(keyedBy:)`.
76 | func container(keyedBy: K.Type) -> KeyedEncodingContainer {
77 | .invalid(at: self.codingPath)
78 | }
79 |
80 | // See `Encoder.unkeyedContainer`.
81 | func unkeyedContainer() -> any UnkeyedEncodingContainer {
82 | .invalid(at: self.codingPath)
83 | }
84 |
85 | // See `Encoder.singleValueContainer`.
86 | func singleValueContainer() -> any SingleValueEncodingContainer {
87 | self
88 | }
89 |
90 | // See `SingleValueEncodingContainer.encodeNil()`.
91 | func encodeNil() throws {
92 | self.value = .null
93 | }
94 |
95 | // See `SingleValueEncodingContainer.encode(_:)`.
96 | func encode(_ value: Bool) throws {
97 | self.value = .init(bool: value)
98 | }
99 |
100 | // See `SingleValueEncodingContainer.encode(_:)`.
101 | func encode(_ value: String) throws {
102 | self.value = .init(string: value)
103 | }
104 |
105 | // See `SingleValueEncodingContainer.encode(_:)`.
106 | func encode(_ value: Float) throws {
107 | self.value = .init(float: value)
108 | }
109 |
110 | // See `SingleValueEncodingContainer.encode(_:)`.
111 | func encode(_ value: Double) throws {
112 | self.value = .init(double: value)
113 | }
114 |
115 | // See `SingleValueEncodingContainer.encode(_:)`.
116 | func encode(_ value: Int8) throws {
117 | self.value = .init(int: numericCast(value))
118 | }
119 |
120 | // See `SingleValueEncodingContainer.encode(_:)`.
121 | func encode(_ value: Int16) throws {
122 | self.value = .init(int: numericCast(value))
123 | }
124 |
125 | // See `SingleValueEncodingContainer.encode(_:)`.
126 | func encode(_ value: Int32) throws {
127 | self.value = .init(int: numericCast(value))
128 | }
129 |
130 | // See `SingleValueEncodingContainer.encode(_:)`.
131 | func encode(_ value: Int64) throws {
132 | self.value = .init(int: numericCast(value))
133 | }
134 |
135 | // See `SingleValueEncodingContainer.encode(_:)`.
136 | func encode(_ value: Int) throws {
137 | self.value = .init(int: value)
138 | }
139 |
140 | // See `SingleValueEncodingContainer.encode(_:)`.
141 | func encode(_ value: UInt8) throws {
142 | self.value = .init(int: numericCast(value))
143 | }
144 |
145 | // See `SingleValueEncodingContainer.encode(_:)`.
146 | func encode(_ value: UInt16) throws {
147 | self.value = .init(int: numericCast(value))
148 | }
149 |
150 | // See `SingleValueEncodingContainer.encode(_:)`.
151 | func encode(_ value: UInt32) throws {
152 | self.value = .init(int: numericCast(value))
153 | }
154 |
155 | // See `SingleValueEncodingContainer.encode(_:)`.
156 | func encode(_ value: UInt64) throws {
157 | self.value = .init(int: numericCast(value))
158 | }
159 |
160 | // See `SingleValueEncodingContainer.encode(_:)`.
161 | func encode(_ value: UInt) throws {
162 | self.value = .init(int: numericCast(value))
163 | }
164 |
165 | // See `SingleValueEncodingContainer.encode(_:)`.
166 | func encode(_ value: some Encodable) throws {
167 | self.value = try self.dataEncoder.encode(value)
168 | }
169 | }
170 | }
171 |
172 | /// This conformance suppresses a slew of spurious warnings in some build environments.
173 | extension MySQLNIO.MySQLProtocol.DataType: @unchecked Swift.Sendable {} // Fully qualifying the type names implies @retroactive
174 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLDatabase+SQL.swift:
--------------------------------------------------------------------------------
1 | import MySQLNIO
2 | import SQLKit
3 |
4 | extension MySQLDatabase {
5 | /// Return an object allowing access to this database via the `SQLDatabase` interface.
6 | ///
7 | /// - Parameters:
8 | /// - encoder: A ``MySQLDataEncoder`` used to translate bound query parameters into `MySQLData` values.
9 | /// - decoder: A ``MySQLDataDecoder`` used to translate `MySQLData` values into output values in `SQLRow`s.
10 | /// - Returns: An instance of `SQLDatabase` which accesses the same database as `self`.
11 | public func sql(
12 | encoder: MySQLDataEncoder = .init(),
13 | decoder: MySQLDataDecoder = .init(),
14 | queryLogLevel: Logger.Level? = .debug
15 | ) -> any SQLDatabase {
16 | MySQLSQLDatabase(database: .init(value: self), encoder: encoder, decoder: decoder, queryLogLevel: queryLogLevel)
17 | }
18 | }
19 |
20 | /// Wraps a `MySQLDatabase` with the `SQLDatabase` protocol.
21 | private struct MySQLSQLDatabase: SQLDatabase {
22 | struct FakeSendable: @unchecked Sendable {
23 | let value: T
24 | }
25 |
26 | /// The underlying `MySQLDatabase`.
27 | let database: FakeSendable
28 |
29 | /// A ``MySQLDataEncoder`` used to translate bindings into `MySQLData` values.
30 | let encoder: MySQLDataEncoder
31 |
32 | /// A ``MySQLDataDecoder`` used to translate `MySQLData` values into output values in `SQLRow`s.
33 | let decoder: MySQLDataDecoder
34 |
35 | // See `SQLDatabase.logger`.
36 | var logger: Logger { self.database.value.logger }
37 |
38 | // See `SQLDatabase.eventLoop`.
39 | var eventLoop: any EventLoop { self.database.value.eventLoop }
40 |
41 | // See `SQLDatabase.dialect`.
42 | var dialect: any SQLDialect { MySQLDialect() }
43 |
44 | // See `SQLDatabase.queryLogLevel`.
45 | let queryLogLevel: Logger.Level?
46 |
47 | // See `SQLDatabase.execute(sql:_:)`.
48 | func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture {
49 | let (sql, binds) = self.serialize(query)
50 |
51 | if let queryLogLevel = self.queryLogLevel {
52 | self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(binds.map { .string("\($0)") })])
53 | }
54 |
55 | do {
56 | return try self.database.value.query(
57 | sql,
58 | binds.map { try self.encoder.encode($0) },
59 | onRow: { onRow($0.sql(decoder: self.decoder)) }
60 | )
61 | } catch {
62 | return self.eventLoop.makeFailedFuture(error)
63 | }
64 | }
65 |
66 | // See `SQLDatabase.execute(sql:_:)`.
67 | func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) async throws {
68 | let (sql, binds) = self.serialize(query)
69 |
70 | if let queryLogLevel = self.queryLogLevel {
71 | self.logger.log(level: queryLogLevel, "Executing query", metadata: ["sql": .string(sql), "binds": .array(binds.map { .string("\($0)") })])
72 | }
73 |
74 | return try await self.database.value.query(
75 | sql,
76 | binds.map { try self.encoder.encode($0) },
77 | onRow: { onRow($0.sql(decoder: self.decoder)) }
78 | ).get()
79 | }
80 |
81 | // See `SQLDatabase.withSession(_:)`.
82 | func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R {
83 | try await self.database.value.withConnection { c in
84 | let sqlDb = c.sql(encoder: self.encoder, decoder: self.decoder)
85 |
86 | return sqlDb.eventLoop.makeFutureWithTask { try await closure(sqlDb) }
87 | }.get()
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLDialect.swift:
--------------------------------------------------------------------------------
1 | import enum Crypto.Insecure
2 | import SQLKit
3 | import Foundation
4 |
5 | /// Provides a type conforming to `SQLDialect` specifying the syntax used by MySQL.
6 | public struct MySQLDialect: SQLDialect {
7 | /// Create a new ``MySQLDialect``.
8 | public init() {}
9 |
10 | // See `SQLDialect.name`.
11 | public var name: String {
12 | "mysql"
13 | }
14 |
15 | // See `SQLDialect.identifierQuote`.
16 | public var identifierQuote: any SQLExpression {
17 | SQLRaw("`")
18 | }
19 |
20 | // See `SQLDialect.literalStringQuote`.
21 | public var literalStringQuote: any SQLExpression {
22 | SQLRaw("'")
23 | }
24 |
25 | // See `SQLDialect.bindPlaceholder(at:)`.
26 | public func bindPlaceholder(at position: Int) -> any SQLExpression {
27 | SQLRaw("?")
28 | }
29 |
30 | // See `SQLDialect.literalBoolean(_:)`.
31 | public func literalBoolean(_ value: Bool) -> any SQLExpression {
32 | SQLRaw(value ? "1" : "0")
33 | }
34 |
35 | // See `SQLDialect.autoIncrementClause`.
36 | public var autoIncrementClause: any SQLExpression {
37 | SQLRaw("AUTO_INCREMENT")
38 | }
39 |
40 | // See `SQLDialect.supportsAutoIncrement`.
41 | public var supportsAutoIncrement: Bool {
42 | true
43 | }
44 |
45 | // See `SQLDialect.enumSyntax`.
46 | public var enumSyntax: SQLEnumSyntax {
47 | .inline
48 | }
49 |
50 | // See `SQLDialect.customDataType(for:)`.
51 | public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? {
52 | switch dataType {
53 | case .text:
54 | SQLRaw("VARCHAR(255)")
55 | default:
56 | nil
57 | }
58 | }
59 |
60 | // See `SQLDialect.alterTableSyntax`.
61 | public var alterTableSyntax: SQLAlterTableSyntax {
62 | .init(
63 | alterColumnDefinitionClause: SQLRaw("MODIFY COLUMN"),
64 | alterColumnDefinitionTypeKeyword: nil
65 | )
66 | }
67 |
68 | // See `SQLDialect.normalizeSQLConstraint(identifier:)`.
69 | public func normalizeSQLConstraint(identifier: any SQLExpression) -> any SQLExpression {
70 | if let sqlIdentifier = identifier as? SQLIdentifier {
71 | SQLIdentifier(Insecure.SHA1.hash(data: Data(sqlIdentifier.string.utf8)).hexRepresentation)
72 | } else {
73 | identifier
74 | }
75 | }
76 |
77 | // See `SQLDialect.triggerSyntax`.
78 | public var triggerSyntax: SQLTriggerSyntax {
79 | .init(create: [.supportsBody, .conditionRequiresParentheses, .supportsOrder], drop: [])
80 | }
81 |
82 | // See `SQLDialect.upsertSyntax`.
83 | public var upsertSyntax: SQLUpsertSyntax {
84 | .mysqlLike
85 | }
86 |
87 | // See `SQLDialect.unionFeatures`.
88 | public var unionFeatures: SQLUnionFeatures {
89 | [.union, .unionAll/*, .intersect, .intersectAll, .except, .exceptAll, .explicitDistinct*/, .parenthesizedSubqueries]
90 | }
91 |
92 | // See `SQLDialect.sharedSelectLockExpression`.
93 | public var sharedSelectLockExpression: (any SQLExpression)? {
94 | SQLRaw("LOCK IN SHARE MODE")
95 | }
96 |
97 | // See `SQLDialect.exclusiveSelectLockExpression`.
98 | public var exclusiveSelectLockExpression: (any SQLExpression)? {
99 | SQLRaw("FOR UPDATE")
100 | }
101 |
102 | // See `SQLDialect.nestedSubpathExpression(in:for:)`.
103 | public func nestedSubpathExpression(in column: any SQLExpression, for path: [String]) -> (any SQLExpression)? {
104 | guard !path.isEmpty else { return nil }
105 |
106 | // N.B.: While MySQL has had the `->` and `->>` operators since 5.7.13, there are still implementations with
107 | // which they do not work properly (most notably AWS's Aurora 2.x), so we use the legacy functions instead.
108 | return SQLFunction("json_unquote", args:
109 | SQLFunction("json_extract", args: [
110 | column,
111 | SQLLiteral.string("$.\(path.joined(separator: "."))")
112 | ]
113 | ))
114 | }
115 | }
116 |
117 | fileprivate let hexTable: [UInt8] = [0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66]
118 | extension Sequence where Element == UInt8 {
119 | fileprivate var hexRepresentation: String {
120 | .init(decoding: self.flatMap { [hexTable[Int($0 >> 4)], hexTable[Int($0 & 0xf)]] }, as: Unicode.ASCII.self)
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/MySQLRow+SQL.swift:
--------------------------------------------------------------------------------
1 | import MySQLNIO
2 | import SQLKit
3 |
4 | extension MySQLRow {
5 | /// Return an `SQLRow` interface to this row.
6 | ///
7 | /// - Parameter decoder: A ``MySQLDataDecoder`` used to translate `MySQLData` values into output values in `SQLRow`s.
8 | /// - Returns: An instance of `SQLRow` which accesses the same data as `self`.
9 | public func sql(decoder: MySQLDataDecoder = .init()) -> any SQLRow {
10 | MySQLSQLRow(row: self, decoder: decoder)
11 | }
12 | }
13 |
14 | extension MySQLNIO.MySQLRow: @unchecked Swift.Sendable {} // Fully qualifying the type names implies @retroactive
15 |
16 | /// An error used to signal that a column requested from a `MySQLRow` using the `SQLRow` interface is not present.
17 | struct MissingColumn: Error {
18 | let column: String
19 | }
20 |
21 | /// Wraps a `MySQLRow` with the `SQLRow` protocol.
22 | private struct MySQLSQLRow: SQLRow {
23 | /// The underlying `MySQLRow`.
24 | let row: MySQLRow
25 |
26 | /// A ``MySQLDataDecoder`` used to translate `MySQLData` values into output values.
27 | let decoder: MySQLDataDecoder
28 |
29 | // See `SQLRow.allColumns`.
30 | var allColumns: [String] {
31 | self.row.columnDefinitions.map { $0.name }
32 | }
33 |
34 | // See `SQLRow.contains(column:)`.
35 | func contains(column: String) -> Bool {
36 | self.row.columnDefinitions.contains { $0.name == column }
37 | }
38 |
39 | // See `SQLRow.decodeNil(column:)`.
40 | func decodeNil(column: String) throws -> Bool {
41 | guard let data = self.row.column(column) else {
42 | return true
43 | }
44 | return data.buffer == nil
45 | }
46 |
47 | // See `SQLRow.decode(column:as:)`.
48 | func decode(column: String, as: D.Type) throws -> D {
49 | guard let data = self.row.column(column) else {
50 | throw MissingColumn(column: column)
51 | }
52 | return try self.decoder.decode(D.self, from: data)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/MySQLKit/Optional+MySQLDataConvertible.swift:
--------------------------------------------------------------------------------
1 | import MySQLNIO
2 |
3 | /// Conforms `Optional` to `MySQLDataConvertible` for efficiency.
4 | extension Swift.Optional: MySQLNIO.MySQLDataConvertible where Wrapped: MySQLNIO.MySQLDataConvertible {
5 | // See `MySQLDataConvertible.init?(mysqlData:)`.
6 | public init?(mysqlData: MySQLData) {
7 | if mysqlData.buffer != nil {
8 | guard let value = Wrapped.init(mysqlData: mysqlData) else {
9 | return nil
10 | }
11 | self = .some(value)
12 | } else {
13 | self = .none
14 | }
15 | }
16 |
17 | // See `MySQLDataConvertible.mysqlData`.
18 | public var mysqlData: MySQLData? {
19 | self == nil ? .null : self.flatMap(\.mysqlData)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Tests/MySQLKitTests/MySQLKitTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Logging
3 | import MySQLKit
4 | import SQLKitBenchmark
5 | import XCTest
6 |
7 | final class MySQLKitTests: XCTestCase {
8 | func testSQLBenchmark() async throws {
9 | try await SQLBenchmarker(on: self.mysql.sql()).runAllTests()
10 | }
11 |
12 | func testNullDecode() async throws {
13 | struct Person: Codable {
14 | let id: Int
15 | let name: String?
16 | }
17 |
18 | let rows = try await self.mysql.sql().raw("SELECT 1 as `id`, null as `name`")
19 | .all(decoding: Person.self)
20 | XCTAssertEqual(rows[0].id, 1)
21 | XCTAssertEqual(rows[0].name, nil)
22 | }
23 |
24 | func testMissingColumnDecode() async throws {
25 | let rows = try await self.mysql.sql().raw("SELECT 1 as `id`, null as `not_name`, 'a' AS `less_name`, 0 AS `not_at_all_name`")
26 | .all()
27 | XCTAssertTrue(try rows[0].decodeNil(column: "name"))
28 | XCTAssertThrowsError(try rows[0].decode(column: "name", as: String.self))
29 | XCTAssertNil(try rows[0].decode(column: "not_name", as: String?.self))
30 | XCTAssertThrowsError(try rows[0].decode(column: "less_name", as: Int?.self))
31 | XCTAssertEqual(try rows[0].decode(column: "not_at_all_name", as: Int?.self), 0)
32 | }
33 |
34 | func testCustomJSONCoder() async throws {
35 | let encoder = JSONEncoder()
36 | encoder.dateEncodingStrategy = .secondsSince1970
37 |
38 | let decoder = JSONDecoder()
39 | decoder.dateDecodingStrategy = .secondsSince1970
40 |
41 | let db = self.mysql.sql(encoder: .init(json: encoder), decoder: .init(json: decoder))
42 |
43 | struct Foo: Codable, Equatable {
44 | var bar: Bar
45 | }
46 | struct Bar: Codable, Equatable {
47 | var baz: Date
48 | }
49 |
50 | try await db.drop(table: "foo").ifExists().run()
51 | try await db.create(table: "foo")
52 | .column("bar", type: .custom(SQLRaw("JSON")))
53 | .run()
54 | do {
55 | let foo = Foo(bar: .init(baz: .init(timeIntervalSince1970: 1337)))
56 | try await db.insert(into: "foo").model(foo).run()
57 |
58 | let rows = try await db.select().columns("*").from("foo").all(decoding: Foo.self)
59 | XCTAssertEqual(rows, [foo])
60 | } catch {
61 | try? await db.drop(table: "foo").ifExists().run()
62 | }
63 | try await db.drop(table: "foo").ifExists().run()
64 | }
65 |
66 | /// Tests dealing with encoding of values whose `encode(to:)` implementation calls one of the `superEncoder()`
67 | /// methods (most notably the implementation of `Codable` for Fluent's `Fields`, which we can't directly test
68 | /// at this layer).
69 | func testValuesThatUseSuperEncoder() throws {
70 | struct UnusualType: Codable {
71 | var prop1: String, prop2: [Bool], prop3: [[Bool]]
72 |
73 | // This is intentionally contrived - Fluent's implementation does Codable this roundabout way as a
74 | // workaround for the interaction of property wrappers with optional properties; it serves no purpose
75 | // here other than to demonstrate that the encoder supports it.
76 | private enum CodingKeys: String, CodingKey {
77 | case prop1, prop2, prop3
78 | }
79 |
80 | init(prop1: String, prop2: [Bool], prop3: [[Bool]]) {
81 | (self.prop1, self.prop2, self.prop3) = (prop1, prop2, prop3)
82 | }
83 |
84 | init(from decoder: any Decoder) throws {
85 | let container = try decoder.container(keyedBy: CodingKeys.self)
86 | self.prop1 = try .init(from: container.superDecoder(forKey: .prop1))
87 |
88 | var acontainer = try container.nestedUnkeyedContainer(forKey: .prop2), ongoing: [Bool] = []
89 | while !acontainer.isAtEnd { ongoing.append(try Bool.init(from: acontainer.superDecoder())) }
90 | self.prop2 = ongoing
91 |
92 | var bcontainer = try container.nestedUnkeyedContainer(forKey: .prop3), bongoing: [[Bool]] = []
93 | while !bcontainer.isAtEnd {
94 | var ccontainer = try bcontainer.nestedUnkeyedContainer(), congoing: [Bool] = []
95 |
96 | while !ccontainer.isAtEnd { congoing.append(try Bool.init(from: ccontainer.superDecoder())) }
97 | bongoing.append(congoing)
98 | }
99 | self.prop3 = bongoing
100 | }
101 |
102 | func encode(to encoder: any Encoder) throws {
103 | var container = encoder.container(keyedBy: CodingKeys.self)
104 | try self.prop1.encode(to: container.superEncoder(forKey: .prop1))
105 |
106 | var acontainer = container.nestedUnkeyedContainer(forKey: .prop2)
107 | for val in self.prop2 { try val.encode(to: acontainer.superEncoder()) }
108 |
109 | var bcontainer = container.nestedUnkeyedContainer(forKey: .prop3)
110 | for arr in self.prop3 {
111 | var ccontainer = bcontainer.nestedUnkeyedContainer()
112 |
113 | for val in arr { try val.encode(to: ccontainer.superEncoder()) }
114 | }
115 | }
116 | }
117 |
118 | let instance = UnusualType(prop1: "hello", prop2: [true, false, false, true], prop3: [[true, true], [false], [true], []])
119 | let encoded1 = try MySQLDataEncoder().encode(instance)
120 | let encoded2 = try MySQLDataEncoder().encode([instance, instance])
121 |
122 | XCTAssertEqual(encoded1.type, .string)
123 | XCTAssertEqual(encoded2.type, .string)
124 |
125 | let decoded1 = try MySQLDataDecoder().decode(UnusualType.self, from: encoded1)
126 | let decoded2 = try MySQLDataDecoder().decode([UnusualType].self, from: encoded2)
127 |
128 | XCTAssertEqual(decoded1.prop3, instance.prop3)
129 | XCTAssertEqual(decoded2.count, 2)
130 | }
131 |
132 | func testMySQLURLFormats() {
133 | let config1 = MySQLConfiguration(url: "mysql+tcp://test_username:test_password@test_hostname:9999/test_database?ssl-mode=DISABLED")
134 | XCTAssertNotNil(config1)
135 | XCTAssertEqual(config1?.database, "test_database")
136 | XCTAssertEqual(config1?.password, "test_password")
137 | XCTAssertEqual(config1?.username, "test_username")
138 | XCTAssertNil(config1?.tlsConfiguration)
139 |
140 | let config2 = MySQLConfiguration(url: "mysql+tcp://test_username@test_hostname")
141 | XCTAssertNotNil(config2)
142 | XCTAssertNil(config2?.database)
143 | XCTAssertEqual(config2?.password, "")
144 | XCTAssertEqual(config2?.username, "test_username")
145 | XCTAssertNotNil(config2?.tlsConfiguration)
146 |
147 | let config3 = MySQLConfiguration(url: "mysql+uds://test_username:test_password@localhost/tmp/mysql.sock?ssl-mode=REQUIRED#test_database")
148 | XCTAssertNotNil(config3)
149 | XCTAssertEqual(config3?.database, "test_database")
150 | XCTAssertEqual(config3?.password, "test_password")
151 | XCTAssertEqual(config3?.username, "test_username")
152 | XCTAssertNotNil(config3?.tlsConfiguration)
153 |
154 | let config4 = MySQLConfiguration(url: "mysql+uds://test_username@/tmp/mysql.sock")
155 | XCTAssertNotNil(config4)
156 | XCTAssertNil(config4?.database)
157 | XCTAssertEqual(config4?.password, "")
158 | XCTAssertEqual(config4?.username, "test_username")
159 | XCTAssertNil(config4?.tlsConfiguration)
160 |
161 | for modestr in ["ssl-mode=DISABLED", "tls-mode=VERIFY_IDENTITY&ssl-mode=DISABLED"] {
162 | let config = MySQLConfiguration(url: "mysql://u@h?\(modestr)")
163 | XCTAssertNotNil(config)
164 | XCTAssertNil(config?.tlsConfiguration)
165 | XCTAssertNil(config?.tlsConfiguration)
166 | }
167 |
168 | for modestr in [
169 | "ssl-mode=PREFERRED", "ssl-mode=REQUIRED", "ssl-mode=VERIFY_CA",
170 | "ssl-mode=VERIFY_IDENTITY", "tls-mode=VERIFY_IDENTITY", "ssl=VERIFY_IDENTITY",
171 | "tls-mode=PREFERRED&ssl-mode=VERIFY_IDENTITY"
172 | ] {
173 | let config = MySQLConfiguration(url: "mysql://u@h?\(modestr)")
174 | XCTAssertNotNil(config, modestr)
175 | XCTAssertNotNil(config?.tlsConfiguration, modestr)
176 | }
177 |
178 | XCTAssertNotNil(MySQLConfiguration(url: "mysql://test_username@test_hostname"))
179 | XCTAssertNotNil(MySQLConfiguration(url: "mysql+tcp://test_username@test_hostname"))
180 | XCTAssertNotNil(MySQLConfiguration(url: "mysql+uds://test_username@/tmp/mysql.sock"))
181 |
182 | XCTAssertNil(MySQLConfiguration(url: "mysql+tcp://test_username:test_password@/test_database"), "should fail when hostname missing")
183 | XCTAssertNil(MySQLConfiguration(url: "mysql+tcp://test_hostname"), "should fail when username missing")
184 | XCTAssertNil(MySQLConfiguration(url: "mysql+tcp://test_username@test_hostname?ssl-mode=absurd"), "should fail when TLS mode invalid")
185 | XCTAssertNil(MySQLConfiguration(url: "mysql+uds://localhost/tmp/mysql.sock?ssl-mode=REQUIRED"), "should fail when username missing")
186 | XCTAssertNil(MySQLConfiguration(url: "mysql+uds:///tmp/mysql.sock"), "should fail when authority missing")
187 | XCTAssertNil(MySQLConfiguration(url: "mysql+uds://test_username@localhost/"), "should fail when path missing")
188 | XCTAssertNil(MySQLConfiguration(url: "mysql+uds://test_username@remotehost/tmp"), "should fail when authority not localhost or empty")
189 | XCTAssertNil(MySQLConfiguration(url: "mysql+uds://test_username@localhost/tmp?ssl-mode=absurd"), "should fail when TLS mode invalid")
190 | XCTAssertNil(MySQLConfiguration(url: "postgres://test_username@remotehost/tmp"), "should fail when scheme is not mysql")
191 |
192 | XCTAssertNil(MySQLConfiguration(url: "$$$://postgres"), "should fail when invalid URL")
193 | }
194 |
195 | var mysql: any MySQLDatabase {
196 | self.pools.database(logger: .init(label: "codes.vapor.mysql"))
197 | }
198 |
199 | var pools: EventLoopGroupConnectionPool!
200 |
201 | override func setUpWithError() throws {
202 | try super.setUpWithError()
203 | XCTAssertTrue(isLoggingConfigured)
204 |
205 | var tls = TLSConfiguration.makeClientConfiguration()
206 | tls.certificateVerification = .none
207 |
208 | let configuration = MySQLConfiguration(
209 | hostname: env("MYSQL_HOSTNAME") ?? "localhost",
210 | port: env("MYSQL_PORT").flatMap(Int.init) ?? MySQLConfiguration.ianaPortNumber,
211 | username: env("MYSQL_USERNAME") ?? "test_username",
212 | password: env("MYSQL_PASSWORD") ?? "test_password",
213 | database: env("MYSQL_DATABASE") ?? "test_database",
214 | tlsConfiguration: tls
215 | )
216 | self.pools = .init(
217 | source: .init(configuration: configuration),
218 | maxConnectionsPerEventLoop: System.coreCount,
219 | requestTimeout: .seconds(30),
220 | logger: .init(label: "codes.vapor.mysql"),
221 | on: MultiThreadedEventLoopGroup.singleton
222 | )
223 | }
224 |
225 | override func tearDownWithError() throws {
226 | try self.pools.syncShutdownGracefully()
227 | self.pools = nil
228 |
229 | try super.tearDownWithError()
230 | }
231 | }
232 |
233 | func env(_ name: String) -> String? {
234 | ProcessInfo.processInfo.environment[name]
235 | }
236 |
237 | let isLoggingConfigured: Bool = {
238 | LoggingSystem.bootstrap { label in
239 | var handler = StreamLogHandler.standardOutput(label: label)
240 | handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .debug
241 | return handler
242 | }
243 | return true
244 | }()
245 |
--------------------------------------------------------------------------------