├── .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 | MySQLKit 3 |
4 |
5 | Documentation 6 | Team Chat 7 | MIT License 8 | Continuous Integration 9 | 10 | Swift 5.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 | 2 | 3 | 13 | 14 | 15 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 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 | --------------------------------------------------------------------------------