diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 680623b..672e272 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -5,6 +5,7 @@ on: tags: - 'v*' - '[0-9]*' + workflow_dispatch: jobs: diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0f7d375..6cf57a0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -15,7 +15,7 @@ jobs: format_check_enabled : true broken_symlink_check_enabled : true unacceptable_language_check_enabled : true - shell_check_enabled : true + shell_check_enabled : false docs_check_enabled : false api_breakage_check_enabled : false license_header_check_enabled : false @@ -27,7 +27,7 @@ jobs: uses: BinaryBirds/github-workflows/.github/workflows/extra_soundness.yml@main with: local_swift_dependencies_check_enabled : true - headers_check_enabled : true + headers_check_enabled : false docc_warnings_check_enabled : true swiftlang_tests: diff --git a/Makefile b/Makefile index 76dbcee..5d2acab 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,10 @@ SHELL=/bin/bash baseUrl = https://raw.githubusercontent.com/BinaryBirds/github-workflows/refs/heads/main/scripts -check: symlinks language deps lint headers +check: symlinks language deps lint headers docc-warnings package + +package: + curl -s $(baseUrl)/check-swift-package.sh | bash symlinks: curl -s $(baseUrl)/check-broken-symlinks.sh | bash diff --git a/Package.resolved b/Package.resolved index 7e0b05b..2d3964e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "93cc0f0ca420ec1c07aff996c5017b9efedf86f062a162baf495851a59d5c0c3", + "originHash" : "3e791080143c7c1715f4ff6a4ff3902d3ac67b0f705edede755fdf0d0720f749", "pins" : [ { "identity" : "feather-database", "kind" : "remoteSourceControl", "location" : "https://github.com/feather-framework/feather-database", "state" : { - "revision" : "147c96803f398c50f5717ce08ebd78a9e3ab7ca7", - "version" : "1.0.0-beta.2" + "revision" : "4ef69e67018c4bdf843858e8976c13b97c3afe4c", + "version" : "1.0.0-beta.3" } }, { diff --git a/Package.swift b/Package.swift index b18384a..2667d51 100644 --- a/Package.swift +++ b/Package.swift @@ -37,7 +37,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/vapor/mysql-nio", from: "1.8.0"), - .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.2"), + .package(url: "https://github.com/feather-framework/feather-database", exact: "1.0.0-beta.3"), // [docc-plugin-placeholder] ], targets: [ diff --git a/README.md b/README.md index 2941e60..34751db 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,30 @@ MySQL/MariaDB driver implementation for the abstract [Feather Database](https://github.com/feather-framework/feather-database) Swift API package. -[ - ![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138) -]( - https://github.com/feather-framework/feather-mysql-database/releases/tag/1.0.0-beta.2 -) +[![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138)](https://github.com/feather-framework/feather-mysql-database/releases/tag/1.0.0-beta.2) ## Features -- đŸ€ MySQL/MariaDB driver for Feather Database -- đŸ˜± Automatic query parameter escaping via Swift string interpolation. -- 🔄 Async sequence query results with `Decodable` row support. -- đŸ§” Designed for modern Swift concurrency -- 📚 DocC-based API Documentation -- ✅ Unit tests and code coverage +- MySQL/MariaDB driver for Feather Database +- Automatic query parameter escaping via Swift string interpolation. +- Async sequence query results with `Decodable` row support. +- Designed for modern Swift concurrency +- DocC-based API Documentation +- Unit tests and code coverage ## Requirements ![Swift 6.1+](https://img.shields.io/badge/Swift-6%2E1%2B-F05138) ![Platforms: Linux, macOS, iOS, tvOS, watchOS, visionOS](https://img.shields.io/badge/Platforms-Linux_%7C_macOS_%7C_iOS_%7C_tvOS_%7C_watchOS_%7C_visionOS-F05138) - -- Swift 6.1+ -- Platforms: - - Linux - - macOS 15+ - - iOS 18+ - - tvOS 18+ - - watchOS 11+ - - visionOS 2+ +- Swift 6.1+ +- Platforms: + - Linux + - macOS 15+ + - iOS 18+ + - tvOS 18+ + - watchOS 11+ + - visionOS 2+ ## Installation @@ -46,19 +41,13 @@ Then add `FeatherMySQLDatabase` to your target dependencies: .product(name: "FeatherMySQLDatabase", package: "feather-mysql-database"), ``` - ## Usage -[ - ![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138) -]( - https://feather-framework.github.io/feather-mysql-database/documentation/feathermysqldatabase/ -) +API documentation is available at the link below: -API documentation is available at the following link. +[![DocC API documentation](https://img.shields.io/badge/DocC-API_documentation-F05138)](https://feather-framework.github.io/feather-mysql-database/) -> [!TIP] -> Avoid calling `database.execute` while in a transaction; use the transaction `connection` instead. +Here is a brief example: ```swift import Logging @@ -98,15 +87,17 @@ let database = MySQLDatabaseClient( ) do { - let result = try await database.execute( - query: #""" - SELECT - VERSION() AS `version` - WHERE - 1=\#(1); - """# - ) - + let result = try await database.withConnection { connection in + try await connection.run( + query: #""" + SELECT + VERSION() AS `version` + WHERE + 1=\#(1); + """# + ) + } + for try await item in result { let version = try item.decode(column: "version", as: String.self) print(version) @@ -126,7 +117,6 @@ catch { > [!WARNING] > This repository is a work in progress, things can break until it reaches v1.0.0. - ## Other database drivers The following database driver implementations are available for use: @@ -137,9 +127,9 @@ The following database driver implementations are available for use: ## Development - Build: `swift build` -- Test: - - local: `swift test` - - using Docker: `make docker-test` +- Test: + - local: `swift test` + - using Docker: `make docker-test` - Format: `make format` - Check: `make check` diff --git a/Sources/FeatherMySQLDatabase/MySQLConnection.swift b/Sources/FeatherMySQLDatabase/MySQLConnection.swift deleted file mode 100644 index 3da3d2b..0000000 --- a/Sources/FeatherMySQLDatabase/MySQLConnection.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// MySQLConnection.swift -// feather-mysql-database -// -// Created by Tibor Bödecs on 2026. 01. 10.. -// - -import FeatherDatabase -import MySQLNIO -import NIOCore - -extension MySQLConnection: @retroactive DatabaseConnection { - - /// Execute a MySQL query on this connection. - /// - /// This wraps `MySQLNIO` query execution and maps errors. - /// - Parameter query: The MySQL query to execute. - /// - Throws: A `DatabaseError` if the query fails. - /// - Returns: A query result containing the returned rows. - @discardableResult - public func execute( - query: MySQLQuery - ) async throws(DatabaseError) -> MySQLQueryResult { - do { - let rows = try await self.query(query.sql, query.bindings).get() - return MySQLQueryResult(elements: rows) - } - catch { - throw .query(error) - } - } -} diff --git a/Sources/FeatherMySQLDatabase/MySQLDatabaseClient.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseClient.swift index 54ae149..d095345 100644 --- a/Sources/FeatherMySQLDatabase/MySQLDatabaseClient.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseClient.swift @@ -14,7 +14,9 @@ import MySQLNIO /// Use this client to execute queries and manage transactions on MySQL. public struct MySQLDatabaseClient: DatabaseClient { - var connection: MySQLConnection + public typealias Connection = MySQLDatabaseConnection + + var connection: MySQLDatabaseConnection var logger: Logger /// Create a MySQL database client. @@ -27,7 +29,10 @@ public struct MySQLDatabaseClient: DatabaseClient { connection: MySQLConnection, logger: Logger ) { - self.connection = connection + self.connection = .init( + connection: connection, + logger: logger + ) self.logger = logger } @@ -36,16 +41,13 @@ public struct MySQLDatabaseClient: DatabaseClient { /// Execute work using the stored connection. /// /// The closure is executed with the current connection. - /// - Parameters: - /// - isolation: The actor isolation for the operation. - /// - closure: A closure that receives the MySQL connection. + /// - Parameter closure: A closure that receives the MySQL connection. /// - Throws: A `DatabaseError` if the connection fails. /// - Returns: The query result produced by the closure. @discardableResult - public func connection( - isolation: isolated (any Actor)? = #isolation, - _ closure: (MySQLConnection) async throws -> sending T - ) async throws(DatabaseError) -> sending T { + public func withConnection( + _ closure: (Connection) async throws -> T + ) async throws(DatabaseError) -> T { do { return try await closure(connection) } @@ -60,23 +62,22 @@ public struct MySQLDatabaseClient: DatabaseClient { /// Execute work inside a MySQL transaction. /// /// The closure runs between `START TRANSACTION` and `COMMIT` with rollback on failure. - /// - Parameters: - /// - isolation: The actor isolation for the operation. - /// - closure: A closure that receives the MySQL connection. + /// - Parameter closure: A closure that receives the MySQL connection. /// - Throws: A `DatabaseError` if transaction handling fails. /// - Returns: The query result produced by the closure. @discardableResult - public func transaction( - isolation: isolated (any Actor)? = #isolation, - _ closure: (MySQLConnection) async throws -> sending T - ) async throws(DatabaseError) -> sending T { + public func withTransaction( + _ closure: (Connection) async throws -> T + ) async throws(DatabaseError) -> T { do { - try await connection.execute(query: "START TRANSACTION;") + try await connection.run(query: "START TRANSACTION;") { _ in } } catch { throw DatabaseError.transaction( - MySQLTransactionError(beginError: error) + MySQLTransactionError( + beginError: error + ) ) } @@ -87,7 +88,7 @@ public struct MySQLDatabaseClient: DatabaseClient { closureHasFinished = true do { - try await connection.execute(query: "COMMIT;") + try await connection.run(query: "COMMIT;") { _ in } } catch { throw DatabaseError.transaction( @@ -104,7 +105,7 @@ public struct MySQLDatabaseClient: DatabaseClient { txError.closureError = error do { - try await connection.execute(query: "ROLLBACK;") + try await connection.run(query: "ROLLBACK;") { _ in } } catch { txError.rollbackError = error diff --git a/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift new file mode 100644 index 0000000..160324f --- /dev/null +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseConnection.swift @@ -0,0 +1,55 @@ +// +// MySQLDatabaseConnection.swift +// feather-mysql-database +// +// Created by Tibor Bödecs on 2026. 01. 10. +// + +import FeatherDatabase +import MySQLNIO +import NIOCore + +public struct MySQLDatabaseConnection: DatabaseConnection, Sendable { + + public typealias Query = MySQLQuery + public typealias RowSequence = MySQLRowSequence + + let connection: MySQLNIO.MySQLConnection + public var logger: Logging.Logger + + /// Execute a MySQL query on this connection. + /// + /// This wraps `MySQLNIO` query execution and maps errors. + /// - Parameters: + /// - query: The MySQL query to execute. + /// - handler: A closure that transforms the result into a generic value. + /// - Throws: A `DatabaseError` if the query fails. + /// - Returns: A query result containing the returned rows. + @discardableResult + public func run( + query: Query, + _ handler: (RowSequence) async throws -> T = { $0 } + ) async throws(DatabaseError) -> T { + do { + let rows = + try await connection.query( + query.sql, + query.bindings + ) + .get() + + return try await handler( + MySQLRowSequence( + elements: rows.map { + .init( + row: $0 + ) + } + ) + ) + } + catch { + throw .query(error) + } + } +} diff --git a/Sources/FeatherMySQLDatabase/MySQLQuery.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift similarity index 98% rename from Sources/FeatherMySQLDatabase/MySQLQuery.swift rename to Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift index d40fe99..cfa3551 100644 --- a/Sources/FeatherMySQLDatabase/MySQLQuery.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseQuery.swift @@ -1,8 +1,8 @@ // -// MySQLQuery.swift +// MySQLDatabaseQuery.swift // feather-mysql-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase diff --git a/Sources/FeatherMySQLDatabase/MySQLRow+DatabaseRow.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseRow.swift similarity index 94% rename from Sources/FeatherMySQLDatabase/MySQLRow+DatabaseRow.swift rename to Sources/FeatherMySQLDatabase/MySQLDatabaseRow.swift index 8c70489..3d0e7f0 100644 --- a/Sources/FeatherMySQLDatabase/MySQLRow+DatabaseRow.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseRow.swift @@ -1,14 +1,16 @@ // -// MySQLRow+DatabaseRow.swift +// MySQLDatabaseRow.swift // feather-mysql-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase import MySQLNIO -extension MySQLRow: @retroactive DatabaseRow { +public struct MySQLRow: DatabaseRow { + + var row: MySQLNIO.MySQLRow struct SingleValueDecoder: Decoder, SingleValueDecodingContainer { @@ -91,7 +93,7 @@ extension MySQLRow: @retroactive DatabaseRow { column: String, as type: T.Type ) throws(DecodingError) -> T { - guard let data = self.column(column) else { + guard let data = row.column(column) else { throw .dataCorrupted( .init( codingPath: [], diff --git a/Sources/FeatherMySQLDatabase/MySQLQueryResult.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseRowSequence.swift similarity index 90% rename from Sources/FeatherMySQLDatabase/MySQLQueryResult.swift rename to Sources/FeatherMySQLDatabase/MySQLDatabaseRowSequence.swift index 1e40179..3a61316 100644 --- a/Sources/FeatherMySQLDatabase/MySQLQueryResult.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseRowSequence.swift @@ -1,17 +1,17 @@ // -// MySQLQueryResult.swift +// MySQLDatabaseRowSequence.swift // feather-mysql-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase -import MySQLNIO /// A query result backed by MySQL rows. /// /// Use this type to iterate or collect MySQL query results. -public struct MySQLQueryResult: DatabaseQueryResult { +public struct MySQLRowSequence: DatabaseRowSequence { + let elements: [MySQLRow] /// An async iterator over MySQL rows. diff --git a/Sources/FeatherMySQLDatabase/MySQLTransactionError.swift b/Sources/FeatherMySQLDatabase/MySQLDatabaseTransactionError.swift similarity index 86% rename from Sources/FeatherMySQLDatabase/MySQLTransactionError.swift rename to Sources/FeatherMySQLDatabase/MySQLDatabaseTransactionError.swift index 03fcdd3..b6255fc 100644 --- a/Sources/FeatherMySQLDatabase/MySQLTransactionError.swift +++ b/Sources/FeatherMySQLDatabase/MySQLDatabaseTransactionError.swift @@ -1,8 +1,8 @@ // -// MySQLTransactionError.swift +// MySQLDatabaseTransactionError.swift // feather-mysql-database // -// Created by Tibor Bödecs on 2026. 01. 10.. +// Created by Tibor Bödecs on 2026. 01. 10. // import FeatherDatabase @@ -24,19 +24,19 @@ public struct MySQLTransactionError: DatabaseTransactionError { /// The error thrown while beginning the transaction. /// /// Set when the `START TRANSACTION` step fails. - public var beginError: Error? + public internal(set) var beginError: Error? /// The error thrown inside the transaction closure. /// /// Set when the closure fails before commit. - public var closureError: Error? + public internal(set) var closureError: Error? /// The error thrown while committing the transaction. /// /// Set when the `COMMIT` step fails. - public var commitError: Error? + public internal(set) var commitError: Error? /// The error thrown while rolling back the transaction. /// /// Set when the `ROLLBACK` step fails. - public var rollbackError: Error? + public internal(set) var rollbackError: Error? /// Create a MySQL transaction error payload. /// @@ -48,7 +48,7 @@ public struct MySQLTransactionError: DatabaseTransactionError { /// - closureError: The error thrown inside the transaction closure. /// - commitError: The error thrown while committing the transaction. /// - rollbackError: The error thrown while rolling back the transaction. - public init( + init( file: String = #fileID, line: Int = #line, beginError: Error? = nil, diff --git a/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift b/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift index b83b1a8..8152bdf 100644 --- a/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift +++ b/Tests/FeatherMySQLDatabaseTests/FeatherMySQLDatabaseTestSuite.swift @@ -97,55 +97,58 @@ struct MySQLDatabaseTestSuite { let planetsTable = "planets_\(suffix)" let moonsTable = "moons_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: moonsTable)`; - """# - ) - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: planetsTable)`; - """# - ) + try await database.withConnection { connection in - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: planetsTable)` ( - `id` INTEGER PRIMARY KEY, - `name` TEXT NOT NULL - ) ENGINE=InnoDB; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: moonsTable)` ( - `id` INTEGER PRIMARY KEY, - `planet_id` INTEGER NOT NULL, - CONSTRAINT `fk_\#(unescaped: moonsTable)` - FOREIGN KEY (`planet_id`) - REFERENCES `\#(unescaped: planetsTable)` (`id`) - ) ENGINE=InnoDB; - """# - ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: moonsTable)`; + """# + ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: planetsTable)`; + """# + ) - do { - _ = try await database.execute( + try await connection.run( query: #""" - INSERT INTO `\#(unescaped: moonsTable)` - (`id`, `planet_id`) - VALUES - (1, 999); + CREATE TABLE `\#(unescaped: planetsTable)` ( + `id` INTEGER PRIMARY KEY, + `name` TEXT NOT NULL + ) ENGINE=InnoDB; """# ) - Issue.record("Expected foreign key constraint violation.") - } - catch DatabaseError.query(let error) { - #expect( - "\(error)".contains("foreign key constraint fails") + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: moonsTable)` ( + `id` INTEGER PRIMARY KEY, + `planet_id` INTEGER NOT NULL, + CONSTRAINT `fk_\#(unescaped: moonsTable)` + FOREIGN KEY (`planet_id`) + REFERENCES `\#(unescaped: planetsTable)` (`id`) + ) ENGINE=InnoDB; + """# ) - } - catch { - Issue.record("Expected database query error to be thrown.") + + do { + _ = try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: moonsTable)` + (`id`, `planet_id`) + VALUES + (1, 999); + """# + ) + Issue.record("Expected foreign key constraint violation.") + } + catch DatabaseError.query(let error) { + #expect( + "\(error)".contains("foreign key constraint fails") + ) + } + catch { + Issue.record("Expected database query error to be thrown.") + } } } } @@ -156,37 +159,42 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "galaxies_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) + try await database.withConnection { connection in - try await database.execute( - query: #""" - CREATE TABLE IF NOT EXISTS `\#(unescaped: table)` ( - `id` INTEGER PRIMARY KEY, - `name` TEXT - ); - """# - ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) - let results = try await database.execute( - query: #""" - SELECT `table_name` - FROM `information_schema`.`tables` - WHERE `table_schema` = DATABASE() - AND `table_name` = '\#(unescaped: table)' - ORDER BY `table_name`; - """# - ) + try await connection.run( + query: #""" + CREATE TABLE IF NOT EXISTS `\#(unescaped: table)` ( + `id` INTEGER PRIMARY KEY, + `name` TEXT + ); + """# + ) + + let results = try await connection.run( + query: #""" + SELECT `table_name` + FROM `information_schema`.`tables` + WHERE `table_schema` = DATABASE() + AND `table_name` = '\#(unescaped: table)' + ORDER BY `table_name`; + """# + ) { try await $0.collect() } - let resultArray = try await results.collect() - #expect(resultArray.count == 1) + #expect(results.count == 1) - let item = resultArray[0] - let name = try item.decode(column: "table_name", as: String.self) - #expect(name == table) + let item = results[0] + let name = try item.decode( + column: "table_name", + as: String.self + ) + #expect(name == table) + } } } @@ -196,49 +204,57 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "galaxies_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE IF NOT EXISTS `\#(unescaped: table)` ( - `id` INTEGER PRIMARY KEY, - `name` TEXT - ); - """# - ) + try await database.withConnection { connection in - let name1 = "Andromeda" - let name2 = "Milky Way" + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE IF NOT EXISTS `\#(unescaped: table)` ( + `id` INTEGER PRIMARY KEY, + `name` TEXT + ); + """# + ) - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `name`) - VALUES - (\#(1), \#(name1)), - (\#(2), \#(name2)); - """# - ) + let name1 = "Andromeda" + let name2 = "Milky Way" - let results = try await database.execute( - query: #""" - SELECT * FROM `\#(unescaped: table)` ORDER BY `name` ASC; - """# - ) + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `name`) + VALUES + (\#(1), \#(name1)), + (\#(2), \#(name2)); + """# + ) + + let results = try await connection.run( + query: #""" + SELECT * FROM `\#(unescaped: table)` ORDER BY `name` ASC; + """# + ) { try await $0.collect() } - let resultArray = try await results.collect() - #expect(resultArray.count == 2) + #expect(results.count == 2) - let item1 = resultArray[0] - let name1result = try item1.decode(column: "name", as: String.self) - #expect(name1result == name1) + let item1 = results[0] + let name1result = try item1.decode( + column: "name", + as: String.self + ) + #expect(name1result == name1) - let item2 = resultArray[1] - let name2result = try item2.decode(column: "name", as: String.self) - #expect(name2result == name2) + let item2 = results[1] + let name2result = try item2.decode( + column: "name", + as: String.self + ) + #expect(name2result == name2) + } } } @@ -248,63 +264,72 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "foo_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `value` TEXT - ); - """# - ) + try await database.withConnection { connection in - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `value`) - VALUES - (1, 'abc'), - (2, NULL); - """# - ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `value` TEXT + ); + """# + ) - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `id`, `value` - FROM `\#(unescaped: table)` - ORDER BY `id`; + INSERT INTO `\#(unescaped: table)` + (`id`, `value`) + VALUES + (1, 'abc'), + (2, NULL); """# ) - .collect() - #expect(result.count == 2) + let result = + try await connection.run( + query: #""" + SELECT `id`, `value` + FROM `\#(unescaped: table)` + ORDER BY `id`; + """# + ) { try await $0.collect() } - let item1 = result[0] - let item2 = result[1] + #expect(result.count == 2) - #expect(try item1.decode(column: "id", as: Int.self) == 1) - #expect(try item2.decode(column: "id", as: Int.self) == 2) + let item1 = result[0] + let item2 = result[1] - #expect(try item1.decode(column: "id", as: Int?.self) == .some(1)) - #expect((try? item1.decode(column: "value", as: Int?.self)) == nil) + #expect(try item1.decode(column: "id", as: Int.self) == 1) + #expect(try item2.decode(column: "id", as: Int.self) == 2) - #expect(try item1.decode(column: "value", as: String.self) == "abc") - #expect( - (try? item2.decode(column: "value", as: String.self)) == nil - ) + #expect( + try item1.decode(column: "id", as: Int?.self) == .some(1) + ) + #expect( + (try? item1.decode(column: "value", as: Int?.self)) == nil + ) - #expect( - (try item1.decode(column: "value", as: String?.self)) - == .some("abc") - ) - #expect( - (try item2.decode(column: "value", as: String?.self)) == .none - ) + #expect( + try item1.decode(column: "value", as: String.self) == "abc" + ) + #expect( + (try? item2.decode(column: "value", as: String.self)) == nil + ) + + #expect( + (try item1.decode(column: "value", as: String?.self)) + == .some("abc") + ) + #expect( + (try item2.decode(column: "value", as: String?.self)) + == .none + ) + } } } @@ -314,56 +339,60 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "foo_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) + try await database.withConnection { connection in - let row1: (Int, String?) = (1, "abc") - let row2: (Int, String?) = (2, nil) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `value` TEXT - ); - """# - ) + let row1: (Int, String?) = (1, "abc") + let row2: (Int, String?) = (2, nil) - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `value`) - VALUES - (\#(row1.0), \#(row1.1)), - (\#(row2.0), \#(row2.1)); - """# - ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `value` TEXT + ); + """# + ) - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `id`, `value` - FROM `\#(unescaped: table)` - ORDER BY `id` ASC; + INSERT INTO `\#(unescaped: table)` + (`id`, `value`) + VALUES + (\#(row1.0), \#(row1.1)), + (\#(row2.0), \#(row2.1)); """# ) - .collect() - #expect(result.count == 2) + let result = + try await connection.run( + query: #""" + SELECT `id`, `value` + FROM `\#(unescaped: table)` + ORDER BY `id` ASC; + """# + ) { try await $0.collect() } - let item1 = result[0] - let item2 = result[1] + #expect(result.count == 2) - #expect(try item1.decode(column: "id", as: Int.self) == 1) - #expect(try item2.decode(column: "id", as: Int.self) == 2) + let item1 = result[0] + let item2 = result[1] - #expect( - try item1.decode(column: "value", as: String?.self) == "abc" - ) - #expect(try item2.decode(column: "value", as: String?.self) == nil) + #expect(try item1.decode(column: "id", as: Int.self) == 1) + #expect(try item2.decode(column: "id", as: Int.self) == 2) + + #expect( + try item1.decode(column: "value", as: String?.self) == "abc" + ) + #expect( + try item2.decode(column: "value", as: String?.self) == nil + ) + } } } @@ -373,46 +402,49 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "widgets_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `name` TEXT NOT NULL - ); - """# - ) - - let insert = MySQLQuery( - unsafeSQL: #""" - INSERT INTO `\#(table)` - (`id`, `name`) - VALUES - (?, ?); - """#, - bindings: [.init(int: 1), .init(string: "gizmo")] - ) + try await database.withConnection { connection in - try await database.execute(query: insert) - - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `name` - FROM `\#(unescaped: table)` - WHERE `id` = 1; + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `name` TEXT NOT NULL + ); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "name", as: String.self) == "gizmo" - ) + let insert = MySQLQuery( + unsafeSQL: #""" + INSERT INTO `\#(table)` + (`id`, `name`) + VALUES + (?, ?); + """#, + bindings: [.init(int: 1), .init(string: "gizmo")] + ) + + try await connection.run(query: insert) + + let result = + try await connection.run( + query: #""" + SELECT `name` + FROM `\#(unescaped: table)` + WHERE `id` = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "gizmo" + ) + } } } @@ -422,44 +454,47 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "notes_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `body` TEXT - ); - """# - ) + try await database.withConnection { connection in - let body: String? = nil - let insert: MySQLQuery = #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `body`) - VALUES - (1, \#(body)); - """# - - try await database.execute(query: insert) - - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `body` - FROM `\#(unescaped: table)` - WHERE `id` = 1; + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `body` TEXT + ); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "body", as: String?.self) == nil - ) + let body: String? = nil + let insert: MySQLQuery = #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `body`) + VALUES + (1, \#(body)); + """# + + try await connection.run(query: insert) + + let result = + try await connection.run( + query: #""" + SELECT `body` + FROM `\#(unescaped: table)` + WHERE `id` = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "body", as: String?.self) + == nil + ) + } } } @@ -469,45 +504,47 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "tags_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `label` TEXT NOT NULL - ); - """# - ) - - let label: MySQLData = .init(string: "alpha") - let insert: MySQLQuery = #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `label`) - VALUES - (1, \#(label)); - """# + try await database.withConnection { connection in - try await database.execute(query: insert) - - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `label` - FROM `\#(unescaped: table)` - WHERE `id` = 1; + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `label` TEXT NOT NULL + ); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "label", as: String.self) - == "alpha" - ) + let label: MySQLData = .init(string: "alpha") + let insert: MySQLQuery = #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `label`) + VALUES + (1, \#(label)); + """# + + try await connection.run(query: insert) + + let result = + try await connection.run( + query: #""" + SELECT `label` + FROM `\#(unescaped: table)` + WHERE `id` = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "label", as: String.self) + == "alpha" + ) + } } } @@ -517,66 +554,54 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "numbers_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `value` TEXT NOT NULL - ); - """# - ) + try await database.withConnection { connection in + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `value` TEXT NOT NULL + ); + """# + ) - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `value`) - VALUES - (1, 'one'), - (2, 'two'); - """# - ) + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `value`) + VALUES + (1, 'one'), + (2, 'two'); + """# + ) - let result = try await database.execute( - query: #""" - SELECT `id`, `value` - FROM `\#(unescaped: table)` - ORDER BY `id`; - """# - ) + let result = try await connection.run( + query: #""" + SELECT `id`, `value` + FROM `\#(unescaped: table)` + ORDER BY `id`; + """# + ) { try await $0.collect() } - var iterator = result.makeAsyncIterator() - let first = await iterator.next() - let second = await iterator.next() - let third = await iterator.next() + #expect(result.count == 2) - #expect(first != nil) - #expect(second != nil) - #expect(third == nil) + let first = result[0] + let second = result[1] - if let first { #expect(try first.decode(column: "id", as: Int.self) == 1) #expect( try first.decode(column: "value", as: String.self) == "one" ) - } - else { - Issue.record("Expected first iterator element to exist.") - } - if let second { #expect(try second.decode(column: "id", as: Int.self) == 2) #expect( try second.decode(column: "value", as: String.self) == "two" ) } - else { - Issue.record("Expected second iterator element to exist.") - } } } @@ -586,44 +611,47 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "widgets_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, - `name` TEXT NOT NULL - ); - """# - ) + try await database.withConnection { connection in - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`name`) - VALUES - ('alpha'), - ('beta'); - """# - ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, + `name` TEXT NOT NULL + ); + """# + ) - let result = try await database.execute( - query: #""" - SELECT `name` - FROM `\#(unescaped: table)` - ORDER BY `id` ASC; - """# - ) + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`name`) + VALUES + ('alpha'), + ('beta'); + """# + ) - let first = try await result.collectFirst() + let result = + try await connection.run( + query: #""" + SELECT `name` + FROM `\#(unescaped: table)` + ORDER BY `id` ASC; + """# + ) { try await $0.collect() } + .first - #expect(first != nil) - #expect( - try first?.decode(column: "name", as: String.self) == "alpha" - ) + #expect( + try result?.decode(column: "name", as: String.self) + == "alpha" + ) + } } } @@ -633,46 +661,48 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "items_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `name` TEXT NOT NULL - ); - """# - ) + try await database.withConnection { connection in - try await database.transaction { connection in - try await connection.execute( + try await connection.run( query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `name`) - VALUES - (1, 'widget'); + DROP TABLE IF EXISTS `\#(unescaped: table)`; """# ) - } - - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `name` - FROM `\#(unescaped: table)` - WHERE `id` = 1; + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `name` TEXT NOT NULL + ); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "name", as: String.self) - == "widget" - ) + try await database.withTransaction { connection in + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `name`) + VALUES + (1, 'widget'); + """# + ) + } + + let result = + try await connection.run( + query: #""" + SELECT `name` + FROM `\#(unescaped: table)` + WHERE `id` = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "name", as: String.self) + == "widget" + ) + } } } @@ -682,23 +712,26 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "dummy_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `name` TEXT NOT NULL - ); - """# - ) + try await database.withConnection { connection in + + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `name` TEXT NOT NULL + ); + """# + ) + } do { - _ = try await database.transaction { connection in - try await connection.execute( + try await database.withTransaction { connection in + try await connection.run( query: #""" INSERT INTO `\#(unescaped: table)` (`id`, `name`) @@ -707,7 +740,7 @@ struct MySQLDatabaseTestSuite { """# ) - return try await connection.execute( + return try await connection.run( query: #""" INSERT INTO `\#(unescaped: table)` (`id`, `name`) @@ -737,16 +770,17 @@ struct MySQLDatabaseTestSuite { ) } - let result = - try await database.execute( - query: #""" - SELECT `id` - FROM `\#(unescaped: table)`; - """# - ) - .collect() + try await database.withConnection { connection in + let result = + try await connection.run( + query: #""" + SELECT `id` + FROM `\#(unescaped: table)`; + """# + ) { try await $0.collect() } - #expect(result.isEmpty) + #expect(result.isEmpty) + } } } @@ -756,46 +790,48 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "measurements_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `value` DOUBLE NOT NULL - ); - """# - ) + try await database.withConnection { connection in - let expected = 1.5 + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `value` DOUBLE NOT NULL + ); + """# + ) - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `value`) - VALUES - (1, \#(expected)); - """# - ) + let expected = 1.5 - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `value` - FROM `\#(unescaped: table)` - WHERE `id` = 1; + INSERT INTO `\#(unescaped: table)` + (`id`, `value`) + VALUES + (1, \#(expected)); """# ) - .collect() - #expect(result.count == 1) - #expect( - try result[0].decode(column: "value", as: Double.self) - == expected - ) + let result = + try await connection.run( + query: #""" + SELECT `value` + FROM `\#(unescaped: table)` + WHERE `id` = 1; + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "value", as: Double.self) + == expected + ) + } } } @@ -805,51 +841,53 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "items_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `value` TEXT - ); - """# - ) + try await database.withConnection { connection in - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `value`) - VALUES - (1, 'abc'); - """# - ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `value` TEXT + ); + """# + ) - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `id` - FROM `\#(unescaped: table)`; + INSERT INTO `\#(unescaped: table)` + (`id`, `value`) + VALUES + (1, 'abc'); """# ) - .collect() - #expect(result.count == 1) + let result = + try await connection.run( + query: #""" + SELECT `id` + FROM `\#(unescaped: table)`; + """# + ) { try await $0.collect() } - do { - _ = try result[0].decode(column: "value", as: String.self) - Issue.record("Expected decoding a missing column to throw.") - } - catch DecodingError.dataCorrupted { + #expect(result.count == 1) - } - catch { - Issue.record( - "Expected a dataCorrupted error for missing column." - ) + do { + _ = try result[0].decode(column: "value", as: String.self) + Issue.record("Expected decoding a missing column to throw.") + } + catch DecodingError.dataCorrupted { + + } + catch { + Issue.record( + "Expected a dataCorrupted error for missing column." + ) + } } } } @@ -860,51 +898,53 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "items_\(suffix)" - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` INTEGER NOT NULL PRIMARY KEY, - `value` TEXT - ); - """# - ) + try await database.withConnection { connection in - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `value`) - VALUES - (1, 'abc'); - """# - ) + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `value` TEXT + ); + """# + ) - let result = - try await database.execute( + try await connection.run( query: #""" - SELECT `value` - FROM `\#(unescaped: table)`; + INSERT INTO `\#(unescaped: table)` + (`id`, `value`) + VALUES + (1, 'abc'); """# ) - .collect() - #expect(result.count == 1) + let result = + try await connection.run( + query: #""" + SELECT `value` + FROM `\#(unescaped: table)`; + """# + ) { try await $0.collect() } - do { - _ = try result[0].decode(column: "value", as: Int.self) - Issue.record("Expected decoding a string as Int to throw.") - } - catch DecodingError.typeMismatch { + #expect(result.count == 1) - } - catch { - Issue.record( - "Expected a typeMismatch error when decoding a string as Int." - ) + do { + _ = try result[0].decode(column: "value", as: Int.self) + Issue.record("Expected decoding a string as Int to throw.") + } + catch DecodingError.typeMismatch { + + } + catch { + Issue.record( + "Expected a typeMismatch error when decoding a string as Int." + ) + } } } } @@ -915,20 +955,22 @@ struct MySQLDatabaseTestSuite { let suffix = randomTableSuffix() let table = "missing_table_\(suffix)" - do { - _ = try await database.execute( - query: #""" - SELECT * - FROM `\#(unescaped: table)`; - """# - ) - Issue.record("Expected query to fail for missing table.") - } - catch DatabaseError.query(let error) { - #expect("\(error)".contains("doesn't exist")) - } - catch { - Issue.record("Expected database query error to be thrown.") + try await database.withConnection { connection in + do { + _ = try await connection.run( + query: #""" + SELECT * + FROM `\#(unescaped: table)`; + """# + ) + Issue.record("Expected query to fail for missing table.") + } + catch DatabaseError.query(let error) { + #expect("\(error)".contains("doesn't exist")) + } + catch { + Issue.record("Expected database query error to be thrown.") + } } } } @@ -936,63 +978,75 @@ struct MySQLDatabaseTestSuite { @Test func versionCheck() async throws { try await runUsingTestDatabaseClient { database in - let result = try await database.execute( - query: #""" - SELECT - VERSION() AS `version` - WHERE - 1=\#(1); - """# - ) + try await database.withConnection { connection in + let result = try await connection.run( + query: #""" + SELECT + VERSION() AS `version` + WHERE + 1=\#(1); + """# + ) { try await $0.collect() } - let resultArray = try await result.collect() - #expect(resultArray.count == 1) + #expect(result.count == 1) - let item = resultArray[0] - let version = try item.decode(column: "version", as: String.self) - #expect(!version.isEmpty) + let item = result[0] + let version = try item.decode( + column: "version", + as: String.self + ) + #expect(!version.isEmpty) + } } } @Test func sslCheckStatus() async throws { try await runUsingTestDatabaseClient { database in - let result = try await database.execute( - query: #""" - SHOW VARIABLES LIKE 'have_ssl'; - """# - ) + try await database.withConnection { connection in + let result = try await connection.run( + query: #""" + SHOW VARIABLES LIKE 'have_ssl'; + """# + ) { try await $0.collect() } - let resultArray = try await result.collect() - #expect(resultArray.count == 1) + #expect(result.count == 1) - let item = resultArray[0] - let name = try item.decode(column: "Variable_name", as: String.self) - #expect(name == "have_ssl") + let item = result[0] + let name = try item.decode( + column: "Variable_name", + as: String.self + ) + #expect(name == "have_ssl") - let value = try item.decode(column: "Value", as: String.self) - #expect(value == "YES") + let value = try item.decode(column: "Value", as: String.self) + #expect(value == "YES") + } } } @Test func sslCheckCypher() async throws { try await runUsingTestDatabaseClient { database in - let result = try await database.execute( - query: #""" - SHOW SESSION STATUS LIKE "ssl_cipher"; - """# - ) + try await database.withConnection { connection in + let result = try await connection.run( + query: #""" + SHOW SESSION STATUS LIKE "ssl_cipher"; + """# + ) { try await $0.collect() } - let resultArray = try await result.collect() - #expect(resultArray.count == 1) + #expect(result.count == 1) - let item = resultArray[0] - let name = try item.decode(column: "Variable_name", as: String.self) - #expect(name == "Ssl_cipher") + let item = result[0] + let name = try item.decode( + column: "Variable_name", + as: String.self + ) + #expect(name == "Ssl_cipher") - let value = try item.decode(column: "Value", as: String.self) - #expect(value == "TLS_AES_128_GCM_SHA256") + let value = try item.decode(column: "Value", as: String.self) + #expect(value == "TLS_AES_128_GCM_SHA256") + } } } @@ -1007,42 +1061,45 @@ struct MySQLDatabaseTestSuite { case missingRow } - try await database.execute( - query: #""" - DROP TABLE IF EXISTS `\#(unescaped: table)`; - """# - ) - try await database.execute( - query: #""" - CREATE TABLE `\#(unescaped: table)` ( - `id` VARCHAR(255) NOT NULL PRIMARY KEY, - `access_token` TEXT NOT NULL, - `access_expires_at` TIMESTAMP NOT NULL, - `refresh_token` TEXT NOT NULL, - `refresh_count` INTEGER NOT NULL DEFAULT 0 - ); - """# - ) + try await database.withConnection { connection in - // set an expired token - try await database.execute( - query: #""" - INSERT INTO `\#(unescaped: table)` - (`id`, `access_token`, `access_expires_at`, `refresh_token`, `refresh_count`) - VALUES - ( - \#(sessionID), - 'stale', - NOW() - INTERVAL 5 MINUTE, - 'refresh', - 0 + try await connection.run( + query: #""" + DROP TABLE IF EXISTS `\#(unescaped: table)`; + """# + ) + try await connection.run( + query: #""" + CREATE TABLE `\#(unescaped: table)` ( + `id` VARCHAR(255) NOT NULL PRIMARY KEY, + `access_token` TEXT NOT NULL, + `access_expires_at` TIMESTAMP NOT NULL, + `refresh_token` TEXT NOT NULL, + `refresh_count` INTEGER NOT NULL DEFAULT 0 ); - """# - ) + """# + ) + + // set an expired token + try await connection.run( + query: #""" + INSERT INTO `\#(unescaped: table)` + (`id`, `access_token`, `access_expires_at`, `refresh_token`, `refresh_count`) + VALUES + ( + \#(sessionID), + 'stale', + NOW() - INTERVAL 5 MINUTE, + 'refresh', + 0 + ); + """# + ) + } func getValidAccessToken(sessionID: String) async throws -> String { - try await database.transaction { connection in - let result = try await connection.execute( + try await database.withTransaction { connection in + let result = try await connection.run( query: #""" SELECT `access_token`, @@ -1052,10 +1109,9 @@ struct MySQLDatabaseTestSuite { WHERE `id` = \#(sessionID) FOR UPDATE; """# - ) - let rows = try await result.collect() + ) { try await $0.collect() } - guard let row = rows.first else { + guard let row = result.first else { throw TestError.missingRow } @@ -1081,7 +1137,7 @@ struct MySQLDatabaseTestSuite { try await Task.sleep(for: .milliseconds(40)) - _ = try await connection.execute( + try await connection.run( query: #""" UPDATE `\#(unescaped: table)` SET @@ -1111,32 +1167,35 @@ struct MySQLDatabaseTestSuite { #expect(Set(tokens).count == 1) - let result = - try await database.execute( - query: #""" - SELECT - `access_token`, - `refresh_count`, - `access_expires_at` > NOW() AS `is_valid` - FROM `\#(unescaped: table)` - WHERE `id` = \#(sessionID); - """# - ) - .collect() + try await database.withConnection { connection in - #expect(result.count == 1) - #expect( - try result[0].decode(column: "refresh_count", as: Int.self) - == 1 - ) - #expect( - try result[0].decode(column: "access_token", as: String.self) - == "token_1" - ) - #expect( - try result[0].decode(column: "is_valid", as: Bool.self) - == true - ) + let result = + try await connection.run( + query: #""" + SELECT + `access_token`, + `refresh_count`, + `access_expires_at` > NOW() AS `is_valid` + FROM `\#(unescaped: table)` + WHERE `id` = \#(sessionID); + """# + ) { try await $0.collect() } + + #expect(result.count == 1) + #expect( + try result[0].decode(column: "refresh_count", as: Int.self) + == 1 + ) + #expect( + try result[0] + .decode(column: "access_token", as: String.self) + == "token_1" + ) + #expect( + try result[0].decode(column: "is_valid", as: Bool.self) + == true + ) + } } }