From 8b0a44b38e143f5fea25edac50fa30ce6c5ba465 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:20:27 +0100 Subject: [PATCH 01/11] Remove NIO as a target dependency --- Package.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Package.swift b/Package.swift index 48cc9bef..12282170 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,6 @@ let package = Package( dependencies: [ .product(name: "Collections", package: "swift-collections"), .product(name: "Logging", package: "swift-log"), - .product(name: "NIOCore", package: "swift-nio"), ], swiftSettings: swiftSettings ), From 2dac6a704f44ae95f2cb28676c98817f3820955f Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:26:33 +0100 Subject: [PATCH 02/11] Start removing futures --- .../Builders/Prototypes/SQLQueryBuilder.swift | 16 -- .../Builders/Prototypes/SQLQueryFetcher.swift | 206 +----------------- Sources/SQLKit/Exports.swift | 2 - 3 files changed, 3 insertions(+), 221 deletions(-) diff --git a/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift index eb258737..e6c4b23b 100644 --- a/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift +++ b/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift @@ -1,5 +1,3 @@ -import class NIOCore.EventLoopFuture - /// Base definitions for builders which set up queries and execute them against a given database. /// /// Almost all concrete builders conform to this protocol. @@ -9,12 +7,6 @@ public protocol SQLQueryBuilder: AnyObject { /// Connection to execute query on. var database: any SQLDatabase { get } - - /// Execute the query on the connection, ignoring any results. - /// - /// Although it is a protocol requirement for historical reasons, this is considered a legacy interface - /// thanks to its reliance on `EventLoopFuture`. Users should call ``run()-3tldd`` whenever possible. - func run() -> EventLoopFuture /// Execute the query on the connection, ignoring any results. func run() async throws @@ -22,14 +14,6 @@ public protocol SQLQueryBuilder: AnyObject { } extension SQLQueryBuilder { - /// Execute the query associated with the builder on the builder's database, ignoring any results. - /// - /// See ``SQLQueryFetcher`` for methods which retrieve results from a query. - @inlinable - public func run() -> EventLoopFuture { - self.database.execute(sql: self.query) { _ in } - } - /// Execute the query associated with the builder on the builder's database, ignoring any results. /// /// See ``SQLQueryFetcher`` for methods which retrieve results from a query. diff --git a/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift b/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift index f6c9489c..618da8c8 100644 --- a/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift +++ b/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift @@ -1,76 +1,7 @@ -import class NIOCore.EventLoopFuture - /// Common definitions for ``SQLQueryBuilder``s which support retrieving result rows. public protocol SQLQueryFetcher: SQLQueryBuilder {} -// MARK: - First (EventLoopFuture) - -extension SQLQueryFetcher { - /// Returns the named column from the first output row, if any, decoded as a given type. - /// - /// - Parameters: - /// - column: The name of the column to decode. - /// - type: The type of the desired value. - /// - Returns: A future containing the decoded value, if any. - @inlinable - public func first(decodingColumn column: String, as type: D.Type) -> EventLoopFuture { - self.first().flatMapThrowing { try $0?.decode(column: column, as: D.self) } - } - - /// Using a default-configured ``SQLRowDecoder``, returns the first output row, if any, decoded as a given type. - /// - /// - Parameter type: The type of the desired value. - /// - Returns: A future containing the decoded value, if any. - @inlinable - public func first(decoding type: D.Type) -> EventLoopFuture { - self.first(decoding: D.self, with: .init()) - } - - /// Configure a new ``SQLRowDecoder`` as specified and use it to decode and return the first output row, if any, - /// as a given type. - /// - /// - Parameters: - /// - type: The type of the desired value. - /// - prefix: See ``SQLRowDecoder/prefix``. - /// - keyDecodingStrategy: See ``SQLRowDecoder/keyDecodingStrategy-swift.property``. - /// - userInfo: See ``SQLRowDecoder/userInfo``. - /// - Returns: A future containing the decoded value, if any. - @inlinable - public func first( - decoding type: D.Type, - prefix: String? = nil, - keyDecodingStrategy: SQLRowDecoder.KeyDecodingStrategy = .useDefaultKeys, - userInfo: [CodingUserInfoKey: any Sendable] = [:] - ) -> EventLoopFuture { - self.first(decoding: D.self, with: .init(prefix: prefix, keyDecodingStrategy: keyDecodingStrategy, userInfo: userInfo)) - } - - /// Using the given ``SQLRowDecoder``, returns the first output row, if any, decoded as a given type. - /// - /// - Parameters: - /// - type: The type of the desired value. - /// - decoder: A configured ``SQLRowDecoder`` to use. - /// - Returns: A future containing the decoded value, if any. - @inlinable - public func first(decoding type: D.Type, with decoder: SQLRowDecoder) -> EventLoopFuture { - self.first().flatMapThrowing { try $0?.decode(model: D.self, with: decoder) } - } - - /// Returns the first output row, if any. - /// - /// If `self` conforms to ``SQLPartialResultBuilder``, ``SQLPartialResultBuilder/limit(_:)`` is used to avoid - /// loading more rows than necessary from the database. - /// - /// - Returns: A future containing the first output row, if any. - @inlinable - public func first() -> EventLoopFuture<(any SQLRow)?> { - (self as? any SQLPartialResultBuilder)?.limit(1) - nonisolated(unsafe) var rows = [any SQLRow]() - return self.run { if rows.isEmpty { rows.append($0) } }.map { rows.first } - } -} - -// MARK: - First (async) +// MARK: - First extension SQLQueryFetcher { /// Returns the named column from the first output row, if any, decoded as a given type. @@ -142,70 +73,7 @@ extension SQLQueryFetcher { } } -// MARK: - All (EventLoopFuture) - -extension SQLQueryFetcher { - /// Returns the named column from each output row, if any, decoded as a given type. - /// - /// - Parameters: - /// - column: The name of the column to decode. - /// - type: The type of the desired values. - /// - Returns: A future containing the decoded values, if any. - @inlinable - public func all(decodingColumn column: String, as type: D.Type) -> EventLoopFuture<[D]> { - self.all().flatMapThrowing { try $0.map { try $0.decode(column: column, as: D.self) } } - } - - /// Using a default-configured ``SQLRowDecoder``, returns all output rows, if any, decoded as a given type. - /// - /// - Parameter type: The type of the desired values. - /// - Returns: A future containing the decoded values, if any. - @inlinable - public func all(decoding type: D.Type) -> EventLoopFuture<[D]> { - self.all(decoding: D.self, with: .init()) - } - - /// Configure a new ``SQLRowDecoder`` as specified and use it to decode and return the output rows, if any, - /// as a given type. - /// - /// - Parameters: - /// - type: The type of the desired values. - /// - prefix: See ``SQLRowDecoder/prefix``. - /// - keyDecodingStrategy: See ``SQLRowDecoder/keyDecodingStrategy-swift.property``. - /// - userInfo: See ``SQLRowDecoder/userInfo``. - /// - Returns: A future containing the decoded values, if any. - @inlinable - public func all( - decoding type: D.Type, - prefix: String? = nil, - keyDecodingStrategy: SQLRowDecoder.KeyDecodingStrategy = .useDefaultKeys, - userInfo: [CodingUserInfoKey: any Sendable] = [:] - ) -> EventLoopFuture<[D]> { - self.all(decoding: D.self, with: .init(prefix: prefix, keyDecodingStrategy: keyDecodingStrategy, userInfo: userInfo)) - } - - /// Using the given ``SQLRowDecoder``, returns the output rows, if any, decoded as a given type. - /// - /// - Parameters: - /// - type: The type of the desired values. - /// - decoder: A configured ``SQLRowDecoder`` to use. - /// - Returns: A future containing the decoded values, if any. - @inlinable - public func all(decoding type: D.Type, with decoder: SQLRowDecoder) -> EventLoopFuture<[D]> { - self.all().flatMapThrowing { try $0.map { try $0.decode(model: D.self, with: decoder) } } - } - - /// Returns all output rows, if any. - /// - /// - Returns: A future containing the output rows, if any. - @inlinable - public func all() -> EventLoopFuture<[any SQLRow]> { - nonisolated(unsafe) var rows = [any SQLRow]() - return self.run { row in rows.append(row) }.map { rows } - } -} - -// MARK: - All (async) +// MARK: - All extension SQLQueryFetcher { /// Returns the named column from each output row, if any, decoded as a given type. @@ -269,75 +137,7 @@ extension SQLQueryFetcher { } } -// MARK: - Run (EventLoopFuture) - -extension SQLQueryFetcher { - /// Using a default-configured ``SQLRowDecoder``, call the provided handler closure with the result of decoding - /// each output row, if any, as a given type. - /// - /// - Parameters: - /// - type: The type of the desired values. - /// - handler: A closure which receives the result of each decoding operation, row by row. - /// - Returns: A completion future. - @preconcurrency - @inlinable - public func run(decoding type: D.Type, _ handler: @escaping @Sendable (Result) -> ()) -> EventLoopFuture { - self.run(decoding: D.self, with: .init(), handler) - } - - /// Configure a new ``SQLRowDecoder`` as specified, use it to to decode each output row, if any, as a given type, - /// and call the provided handler closure with each decoding result. - /// - /// - Parameters: - /// - type: The type of the desired values. - /// - prefix: See ``SQLRowDecoder/prefix``. - /// - keyDecodingStrategy: See ``SQLRowDecoder/keyDecodingStrategy-swift.property``. - /// - userInfo: See ``SQLRowDecoder/userInfo``. - /// - handler: A closure which receives the result of each decoding operation, row by row. - /// - Returns: A completion future. - @preconcurrency - @inlinable - public func run( - decoding type: D.Type, - prefix: String? = nil, - keyDecodingStrategy: SQLRowDecoder.KeyDecodingStrategy = .useDefaultKeys, - userInfo: [CodingUserInfoKey: any Sendable] = [:], - _ handler: @escaping @Sendable (Result) -> () - ) -> EventLoopFuture { - self.run(decoding: D.self, with: .init(prefix: prefix, keyDecodingStrategy: keyDecodingStrategy, userInfo: userInfo), handler) - } - - /// Using the given ``SQLRowDecoder``, call the provided handler closure with the result of decoding each output - /// row, if any, as a given type. - /// - /// - Parameters: - /// - type: The type of the desired values. - /// - decoder: A configured ``SQLRowDecoder`` to use. - /// - handler: A closure which receives the result of each decoding operation, row by row. - /// - Returns: A completion future. - @preconcurrency - @inlinable - public func run( - decoding type: D.Type, - with decoder: SQLRowDecoder, - _ handler: @escaping @Sendable (Result) -> () - ) -> EventLoopFuture { - self.run { row in handler(.init { try row.decode(model: D.self, with: decoder) }) } - } - - /// Run the query specified by the builder, calling the provided handler closure with each output row, if any, as - /// it is received. - /// - /// - Parameter handler: A closure which receives each output row one at a time. - /// - Returns: A completion future. - @preconcurrency - @inlinable - public func run(_ handler: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture { - self.database.execute(sql: self.query, handler) - } -} - -// MARK: - Run (async) +// MARK: - Run extension SQLQueryFetcher { /// Using a default-configured ``SQLRowDecoder``, call the provided handler closure with the result of decoding diff --git a/Sources/SQLKit/Exports.swift b/Sources/SQLKit/Exports.swift index 370dd880..a24cd585 100644 --- a/Sources/SQLKit/Exports.swift +++ b/Sources/SQLKit/Exports.swift @@ -1,3 +1 @@ -@_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop -@_documentation(visibility: internal) @_exported import class NIOCore.EventLoopFuture @_documentation(visibility: internal) @_exported import struct Logging.Logger From 4df08fa9d09e521aab8a4650dcd1dd77b32bc12a Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:27:04 +0100 Subject: [PATCH 03/11] Clean up the docs --- .../Docs.docc/SQLDatabase+ExtensionDocs.md | 1 - .../Docs.docc/SQLQueryFetcher+ExtensionDocs.md | 17 ----------------- 2 files changed, 18 deletions(-) diff --git a/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md b/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md index 34e0f199..c10763d1 100644 --- a/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md +++ b/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md @@ -5,7 +5,6 @@ ### Properties - ``SQLDatabase/logger`` -- ``SQLDatabase/eventLoop`` - ``SQLDatabase/version`` - ``SQLDatabase/dialect`` - ``SQLDatabase/queryLogLevel`` diff --git a/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md b/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md index c1af142c..053539d0 100644 --- a/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md +++ b/Sources/SQLKit/Docs.docc/SQLQueryFetcher+ExtensionDocs.md @@ -24,20 +24,3 @@ - ``SQLQueryFetcher/first(decoding:prefix:keyDecodingStrategy:userInfo:)->D?`` - ``SQLQueryFetcher/first(decoding:with:)->D?`` - ``SQLQueryFetcher/first(decodingColumn:as:)->D?`` - -### Legacy `EventLoopFuture` Interfaces - -- ``SQLQueryFetcher/run(_:)->_`` -- ``SQLQueryFetcher/run(decoding:_:)->_`` -- ``SQLQueryFetcher/run(decoding:prefix:keyDecodingStrategy:userInfo:_:)->_`` -- ``SQLQueryFetcher/run(decoding:with:_:)->_`` -- ``SQLQueryFetcher/all()->EventLoopFuture<[SQLRow]>`` -- ``SQLQueryFetcher/all(decoding:)->EventLoopFuture<[D]>`` -- ``SQLQueryFetcher/all(decoding:prefix:keyDecodingStrategy:userInfo:)->EventLoopFuture<[D]>`` -- ``SQLQueryFetcher/all(decoding:with:)->EventLoopFuture<[D]>`` -- ``SQLQueryFetcher/all(decodingColumn:as:)->EventLoopFuture<[D]>`` -- ``SQLQueryFetcher/first()->EventLoopFuture<(SQLRow)?>`` -- ``SQLQueryFetcher/first(decoding:)->EventLoopFuture`` -- ``SQLQueryFetcher/first(decoding:prefix:keyDecodingStrategy:userInfo:)->EventLoopFuture`` -- ``SQLQueryFetcher/first(decoding:with:)->EventLoopFuture`` -- ``SQLQueryFetcher/first(decodingColumn:as:)->EventLoopFuture`` From c266139ea498ce4e9c02f99bd706dff003bbba3d Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:28:26 +0100 Subject: [PATCH 04/11] Remove the event loop requirement from SQLDatabase --- Sources/SQLKit/Database/SQLDatabase.swift | 44 ----------------------- 1 file changed, 44 deletions(-) diff --git a/Sources/SQLKit/Database/SQLDatabase.swift b/Sources/SQLKit/Database/SQLDatabase.swift index 376186c4..704d6a83 100644 --- a/Sources/SQLKit/Database/SQLDatabase.swift +++ b/Sources/SQLKit/Database/SQLDatabase.swift @@ -1,5 +1,3 @@ -import protocol NIOCore.EventLoop -import class NIOCore.EventLoopFuture import struct Logging.Logger /// The common interface to SQLKit for both drivers and client code. @@ -56,14 +54,6 @@ public protocol SQLDatabase: Sendable { /// The `Logger` used for logging all operations relating to a given database. var logger: Logger { get } - /// The `EventLoop` used for asynchronous operations on a given database. - /// - /// If there is no specific `EventLoop` which handles the database (such as because it is a connection pool which - /// assigns loops to connections at point of use, or because the underlying implementation is based on Swift - /// Concurrency or some other asynchronous execution technology), a single consistent `EventLoop` must be chosen - /// for the database and returned for this property nonetheless. - var eventLoop: any EventLoop { get } - /// The version number the database reports for itself. /// /// The version must be provided via a type conforming to the ``SQLDatabaseReportedVersion`` protocol. If the @@ -105,27 +95,6 @@ public protocol SQLDatabase: Sendable { /// Requests that the given generic SQL query be serialized and executed on the database, and that /// the `onRow` closure be invoked once for each result row the query returns (if any). /// - /// Although it is a protocol requirement for historical reasons, this is considered a legacy interface thanks - /// to its reliance on `EventLoopFuture`. Implementers should implement both this method and - /// ``execute(sql:_:)-7trgm`` if they can, and users should use ``execute(sql:_:)-7trgm`` whenever possible. - /// - /// - Parameters: - /// - query: An ``SQLExpression`` representing a complete query to execute. - /// - onRow: A closure which is invoked once for each result row returned by the query (if any). - /// - Returns: An `EventLoopFuture`. - @preconcurrency - func execute( - sql query: any SQLExpression, - _ onRow: @escaping @Sendable (any SQLRow) -> () - ) -> EventLoopFuture - - /// Requests that the given generic SQL query be serialized and executed on the database, and that - /// the `onRow` closure be invoked once for each result row the query returns (if any). - /// - /// If a concrete type conforming to ``SQLDatabase`` can provide a more efficient Concurrency-based implementation - /// than forwarding the invocation through the legacy `EventLoopFuture`-based API, it should override this method - /// in order to do so. - /// /// - Parameters: /// - query: An ``SQLExpression`` representing a complete query to execute. /// - onRow: A closure which is invoked once for each result row returned by the query (if any). @@ -226,11 +195,6 @@ private struct CustomLoggerSQLDatabase: SQLDatabase { // See `SQLDatabase.logger`. let logger: Logger - - // See `SQLDatabase.eventLoop`. - var eventLoop: any EventLoop { - self.database.eventLoop - } // See `SQLDatabase.version`. var version: (any SQLDatabaseReportedVersion)? { @@ -246,14 +210,6 @@ private struct CustomLoggerSQLDatabase: SQLDatabase { var queryLogLevel: Logger.Level? { self.database.queryLogLevel } - - // See `SQLDatabase.execute(sql:_:)`. - func execute( - sql query: any SQLExpression, - _ onRow: @escaping @Sendable (any SQLRow) -> () - ) -> EventLoopFuture { - self.database.execute(sql: query, onRow) - } // See `SQLDatabase.execute(sql:_:)`. func execute( From f29ae57d71f516eb7282a7350986210b1baf610a Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:29:35 +0100 Subject: [PATCH 05/11] Fix an ELF call --- Sources/SQLKit/Database/SQLDatabase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLKit/Database/SQLDatabase.swift b/Sources/SQLKit/Database/SQLDatabase.swift index 704d6a83..bcd0c4bd 100644 --- a/Sources/SQLKit/Database/SQLDatabase.swift +++ b/Sources/SQLKit/Database/SQLDatabase.swift @@ -175,7 +175,7 @@ extension SQLDatabase { sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () ) async throws { - try await self.execute(sql: query, onRow).get() + try await self.execute(sql: query, onRow) } /// The default implementation for ``withSession(_:)-9b68j``. From 5735478379ef33d8d041b18600d83a15c60dd046 Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:30:58 +0100 Subject: [PATCH 06/11] Tidy up the benchmarker --- Sources/SQLKitBenchmark/SQLBenchmarker.swift | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Sources/SQLKitBenchmark/SQLBenchmarker.swift b/Sources/SQLKitBenchmark/SQLBenchmarker.swift index 964de1dc..c99bbd1f 100644 --- a/Sources/SQLKitBenchmark/SQLBenchmarker.swift +++ b/Sources/SQLKitBenchmark/SQLBenchmarker.swift @@ -1,5 +1,4 @@ import Logging -import NIOCore public import SQLKit import XCTest @@ -20,17 +19,7 @@ public final class SQLBenchmarker: Sendable { try await self.testJSONPaths() } } - - @available(*, deprecated, renamed: "runAllTests()", message: "Use `runAllTests()` instead.") - public func testAll() throws { - try database.eventLoop.makeFutureWithTask { try await self.runAllTests() }.wait() - } - - @available(*, deprecated, renamed: "runAllTests()", message: "Use `runAllTests()` instead.") - public func run() throws { - try self.testAll() - } - + func runTest( _ name: String = #function, _ test: (any SQLDatabase) async throws -> () From 14d753c7ca424d1e207ec33dba188797c410a28f Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:34:44 +0100 Subject: [PATCH 07/11] Tidy up the AsyncTests --- Tests/SQLKitTests/AsyncTests.swift | 85 +++++------------------------- 1 file changed, 13 insertions(+), 72 deletions(-) diff --git a/Tests/SQLKitTests/AsyncTests.swift b/Tests/SQLKitTests/AsyncTests.swift index 3031b1ed..81730920 100644 --- a/Tests/SQLKitTests/AsyncTests.swift +++ b/Tests/SQLKitTests/AsyncTests.swift @@ -1,83 +1,54 @@ -import NIOCore import OrderedCollections import SQLKit import Testing @Suite("Async tests") struct AsyncTests { - @Test("SQLDatabase async and futures") - func SQLDatabaseAsyncAndFutures() async throws { + @Test("SQLDatabase") + func SQLDatabase() async throws { let db = TestDatabase() - try await db.execute(sql: SQLRaw("TEST"), { _ in Issue.record("Should not receive results") }).get() - #expect(db.results[0] == "TEST") - try await db.execute(sql: SQLRaw("TEST"), { _ in Issue.record("Should not receive results") }) #expect(db.results[1] == "TEST") } - @Test("SQLQueryBuilder async and futures") - func SQLQueryBuilderAsyncAndFutures() async throws { + @Test("SQLQueryBuilder") + func SQLQueryBuilder() async throws { let db = TestDatabase() - db.outputs = [TestRow(data: [:])] - try await db.update("a").set("b", to: "c").run().get() - #expect(db.results[0] == "UPDATE ``a`` SET ``b`` = &1") - db.outputs = [TestRow(data: [:])] try await db.update("a").set("b", to: "c").run() - #expect(db.results[1] == "UPDATE ``a`` SET ``b`` = &1") + #expect(db.results[0] == "UPDATE ``a`` SET ``b`` = &1") } - @Test("SQLQueryFetcher run methods async and futures") - func SQLQueryFetcherRunMethodsAsyncAndFutures() async throws { + @Test("SQLQueryFetcher run methods") + func SQLQueryFetcherRunMethods() async throws { let db = TestDatabase() try await db.select().column("a").from("b").run { _ in Issue.record("Should not receive results") } #expect(db.results[0] == "SELECT ``a`` FROM ``b``") - try await db.select().column("a").from("b").run { _ in Issue.record("Should not receive results") }.get() - #expect(db.results[1] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] try await db.select().column("a").from("b").run { #expect($0.allColumns.isEmpty) } #expect(db.results[2] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] - try await db.select().column("a").from("b").run { #expect($0.allColumns.isEmpty) }.get() - #expect(db.results[3] == "SELECT ``a`` FROM ``b``") - try await db.select().column("a").from("b").run(decoding: [String: String].self, { _ in Issue.record("Should not receive results") }) #expect(db.results[4] == "SELECT ``a`` FROM ``b``") - try await db.select().column("a").from("b").run(decoding: [String: String].self, { _ in Issue.record("Should not receive results") }).get() - #expect(db.results[5] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] try await db.select().column("a").from("b").run(decoding: [String: String].self, { #expect((try? $0.get()) != nil) }) #expect(db.results[6] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] - try await db.select().column("a").from("b").run(decoding: [String: String].self, { #expect((try? $0.get()) != nil) }).get() - #expect(db.results[7] == "SELECT ``a`` FROM ``b``") - try await db.select().column("a").from("b").run(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys, { _ in Issue.record("Should not receive results") }) #expect(db.results[8] == "SELECT ``a`` FROM ``b``") - try await db.select().column("a").from("b").run(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys, { _ in Issue.record("Should not receive results") }).get() - #expect(db.results[9] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] try await db.select().column("a").from("b").run(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys, { #expect((try? $0.get()) != nil) }) #expect(db.results[10] == "SELECT ``a`` FROM ``b``") - - db.outputs = [TestRow(data: [:])] - try await db.select().column("a").from("b").run(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys, { #expect((try? $0.get()) != nil) }).get() - #expect(db.results[11] == "SELECT ``a`` FROM ``b``") } - @Test("SQLQueryFetcher all methods async and futures") - func SQLQueryFetcherAllMethodsAsyncAndFutures() async throws { + @Test("SQLQueryFetcher all methods") + func SQLQueryFetcherAllMethods() async throws { let db = TestDatabase() let res0 = try await db.select().column("a").from("b").all() @@ -89,44 +60,24 @@ struct AsyncTests { #expect(res1.count == 1) #expect(db.results[1] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] - let res2 = try await db.select().column("a").from("b").all().get() - #expect(res2.count == 1) - #expect(db.results[2] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] let res3 = try await db.select().column("a").from("b").all(decoding: [String: String].self) #expect(res3.count == 1) #expect(db.results[3] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] - let res4 = try await db.select().column("a").from("b").all(decoding: [String: String].self).get() - #expect(res4.count == 1) - #expect(db.results[4] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] let res5 = try await db.select().column("a").from("b").all(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys) #expect(res5.count == 1) #expect(db.results[5] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: [:])] - let res6 = try await db.select().column("a").from("b").all(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys).get() - #expect(res6.count == 1) - #expect(db.results[6] == "SELECT ``a`` FROM ``b``") - db.outputs = [TestRow(data: ["a": "a"])] let res7 = try await db.select().column("a").from("b").all(decodingColumn: "a", as: String.self) #expect(res7.count == 1) #expect(db.results[7] == "SELECT ``a`` FROM ``b``") - - db.outputs = [TestRow(data: ["a": "a"])] - let res8 = try await db.select().column("a").from("b").all(decodingColumn: "a", as: String.self).get() - #expect(res8.count == 1) - #expect(db.results[8] == "SELECT ``a`` FROM ``b``") } - @Test("SQLQueryFetcher first methods async and futures") - func SQLQueryFetcherFirstMethodsAsyncAndFutures() async throws { + @Test("SQLQueryFetcher first methods") + func SQLQueryFetcherFirstMethods() async throws { let db = TestDatabase() let res0 = try await db.select().column("a").from("b").first() @@ -154,21 +105,11 @@ struct AsyncTests { #expect(db.results[4] == "SELECT ``a`` FROM ``b`` LIMIT 1") db.outputs = [TestRow(data: [:])] - let res5 = try await db.select().column("a").from("b").first().get() - #expect(res5 != nil) - #expect(db.results[5] == "SELECT ``a`` FROM ``b`` LIMIT 1") - - db.outputs = [TestRow(data: [:])] - let res6 = try await db.select().column("a").from("b").first(decoding: [String: String].self).get() - #expect(res6 != nil) - #expect(db.results[6] == "SELECT ``a`` FROM ``b`` LIMIT 1") - - db.outputs = [TestRow(data: [:])] - let res7 = try await db.select().column("a").from("b").first(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys).get() + let res7 = try await db.select().column("a").from("b").first(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys) #expect(res7 != nil) #expect(db.results[7] == "SELECT ``a`` FROM ``b`` LIMIT 1") - let res8 = try await db.select().column("a").from("b").first(decodingColumn: "a", as: String.self).get() + let res8 = try await db.select().column("a").from("b").first(decodingColumn: "a", as: String.self) #expect(res8 == nil) #expect(db.results[8] == "SELECT ``a`` FROM ``b`` LIMIT 1") } From 889c52fc74345439117a79f50f83e5cda7f9285a Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:37:09 +0100 Subject: [PATCH 08/11] Update BaseTests --- Tests/SQLKitTests/BaseTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/SQLKitTests/BaseTests.swift b/Tests/SQLKitTests/BaseTests.swift index d9da5d6f..539754bd 100644 --- a/Tests/SQLKitTests/BaseTests.swift +++ b/Tests/SQLKitTests/BaseTests.swift @@ -167,11 +167,9 @@ struct BaseTests { #expect(db.version == nil) #expect(db.logger.logLevel == Logger(label: "l").logLevel) - #expect(ObjectIdentifier(db.eventLoop) == ObjectIdentifier(sdb.eventLoop)) #expect(db.dialect.name == db.dialect.name) #expect(db.queryLogLevel == db.queryLogLevel) await #expect(throws: Never.self) { try await db.execute(sql: SQLRaw("TEST"), { _ in }) } - await #expect(throws: Never.self) { try await db.execute(sql: SQLRaw("TEST"), { _ in }).get() } } @Test("database default async impl") From 6944355d1b89f45eeb43dd96b31804dd1da9bfbd Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:38:39 +0100 Subject: [PATCH 09/11] Fix the AsyncTests --- Tests/SQLKitTests/AsyncTests.swift | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Tests/SQLKitTests/AsyncTests.swift b/Tests/SQLKitTests/AsyncTests.swift index 81730920..9e199ebc 100644 --- a/Tests/SQLKitTests/AsyncTests.swift +++ b/Tests/SQLKitTests/AsyncTests.swift @@ -9,7 +9,7 @@ struct AsyncTests { let db = TestDatabase() try await db.execute(sql: SQLRaw("TEST"), { _ in Issue.record("Should not receive results") }) - #expect(db.results[1] == "TEST") + #expect(db.results[0] == "TEST") } @Test("SQLQueryBuilder") @@ -30,21 +30,21 @@ struct AsyncTests { db.outputs = [TestRow(data: [:])] try await db.select().column("a").from("b").run { #expect($0.allColumns.isEmpty) } - #expect(db.results[2] == "SELECT ``a`` FROM ``b``") + #expect(db.results[1] == "SELECT ``a`` FROM ``b``") try await db.select().column("a").from("b").run(decoding: [String: String].self, { _ in Issue.record("Should not receive results") }) - #expect(db.results[4] == "SELECT ``a`` FROM ``b``") - + #expect(db.results[2] == "SELECT ``a`` FROM ``b``") + db.outputs = [TestRow(data: [:])] try await db.select().column("a").from("b").run(decoding: [String: String].self, { #expect((try? $0.get()) != nil) }) - #expect(db.results[6] == "SELECT ``a`` FROM ``b``") + #expect(db.results[3] == "SELECT ``a`` FROM ``b``") try await db.select().column("a").from("b").run(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys, { _ in Issue.record("Should not receive results") }) - #expect(db.results[8] == "SELECT ``a`` FROM ``b``") + #expect(db.results[4] == "SELECT ``a`` FROM ``b``") db.outputs = [TestRow(data: [:])] try await db.select().column("a").from("b").run(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys, { #expect((try? $0.get()) != nil) }) - #expect(db.results[10] == "SELECT ``a`` FROM ``b``") + #expect(db.results[5] == "SELECT ``a`` FROM ``b``") } @Test("SQLQueryFetcher all methods") @@ -63,17 +63,17 @@ struct AsyncTests { db.outputs = [TestRow(data: [:])] let res3 = try await db.select().column("a").from("b").all(decoding: [String: String].self) #expect(res3.count == 1) - #expect(db.results[3] == "SELECT ``a`` FROM ``b``") + #expect(db.results[2] == "SELECT ``a`` FROM ``b``") db.outputs = [TestRow(data: [:])] let res5 = try await db.select().column("a").from("b").all(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys) #expect(res5.count == 1) - #expect(db.results[5] == "SELECT ``a`` FROM ``b``") + #expect(db.results[3] == "SELECT ``a`` FROM ``b``") db.outputs = [TestRow(data: ["a": "a"])] let res7 = try await db.select().column("a").from("b").all(decodingColumn: "a", as: String.self) #expect(res7.count == 1) - #expect(db.results[7] == "SELECT ``a`` FROM ``b``") + #expect(db.results[4] == "SELECT ``a`` FROM ``b``") } @Test("SQLQueryFetcher first methods") @@ -107,10 +107,10 @@ struct AsyncTests { db.outputs = [TestRow(data: [:])] let res7 = try await db.select().column("a").from("b").first(decoding: [String: String].self, keyDecodingStrategy: .useDefaultKeys) #expect(res7 != nil) - #expect(db.results[7] == "SELECT ``a`` FROM ``b`` LIMIT 1") + #expect(db.results[5] == "SELECT ``a`` FROM ``b`` LIMIT 1") let res8 = try await db.select().column("a").from("b").first(decodingColumn: "a", as: String.self) #expect(res8 == nil) - #expect(db.results[8] == "SELECT ``a`` FROM ``b`` LIMIT 1") + #expect(db.results[6] == "SELECT ``a`` FROM ``b`` LIMIT 1") } } From d6676eb44f328b311efd79dac9cf29cb5044b2ea Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:42:03 +0100 Subject: [PATCH 10/11] Fully clear up NIO and get the tests passing --- Package.swift | 3 --- Tests/SQLKitTests/BaseTests.swift | 7 ------- Tests/SQLKitTests/TestMocks.swift | 26 -------------------------- 3 files changed, 36 deletions(-) diff --git a/Package.swift b/Package.swift index 12282170..1724167c 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,6 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), - .package(url: "https://github.com/apple/swift-nio.git", from: "2.84.0"), ], targets: [ .target( @@ -37,8 +36,6 @@ let package = Package( .testTarget( name: "SQLKitTests", dependencies: [ - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOEmbedded", package: "swift-nio"), .target(name: "SQLKit"), .target(name: "SQLKitBenchmark"), ], diff --git a/Tests/SQLKitTests/BaseTests.swift b/Tests/SQLKitTests/BaseTests.swift index 539754bd..7a53a31d 100644 --- a/Tests/SQLKitTests/BaseTests.swift +++ b/Tests/SQLKitTests/BaseTests.swift @@ -1,7 +1,5 @@ @testable import SQLKit import struct Logging.Logger -import protocol NIOCore.EventLoop -import class NIOCore.EventLoopFuture import SQLKitBenchmark import Testing @@ -171,11 +169,6 @@ struct BaseTests { #expect(db.queryLogLevel == db.queryLogLevel) await #expect(throws: Never.self) { try await db.execute(sql: SQLRaw("TEST"), { _ in }) } } - - @Test("database default async impl") - func databaseDefaultAsyncImpl() async throws { - await #expect(throws: Never.self) { try await TestNoAsyncDatabase().execute(sql: SQLRaw("TEST"), { _ in }) } - } @Test("database version") func databaseVersion() { diff --git a/Tests/SQLKitTests/TestMocks.swift b/Tests/SQLKitTests/TestMocks.swift index 85d3a12d..7b6a0db1 100644 --- a/Tests/SQLKitTests/TestMocks.swift +++ b/Tests/SQLKitTests/TestMocks.swift @@ -1,12 +1,6 @@ import OrderedCollections public import SQLKit -import protocol NIOCore.EventLoop -import class NIOCore.EventLoopFuture -import class NIOEmbedded.NIOAsyncTestingEventLoop import Logging -#if canImport(Dispatch) -import class Dispatch.DispatchQueue -#endif extension SQLQueryBuilder { /// Serialize this builder's query and return the textual SQL, discarding any bindings. @@ -28,23 +22,11 @@ extension SQLQueryBuilder { /// its internal arrays of accumulated "results". Most things about its dialect are mutable. final class TestDatabase: SQLDatabase, @unchecked Sendable { let logger: Logger = { var l = Logger(label: "codes.vapor.sql.test"); l.logLevel = .debug; return l }() - let eventLoop: any EventLoop = NIOAsyncTestingEventLoop() var results: [String] = [] var bindResults: [[any Encodable & Sendable]] = [] var outputs: [any SQLRow] = [] var dialect: any SQLDialect { self._dialect } var _dialect: GenericDialect = .init() - - func execute(sql query: any SQLExpression, _ onRow: @escaping (any SQLRow) -> ()) -> EventLoopFuture { - let (sql, binds) = self.serialize(query) - - self.results.append(sql) - self.bindResults.append(binds) - while let row = self.outputs.popLast() { - onRow(row) - } - return self.eventLoop.makeSucceededFuture(()) - } func execute(sql query: any SQLExpression, _ onRow: @escaping (any SQLRow) -> ()) async throws { let (sql, binds) = self.serialize(query) @@ -145,11 +127,3 @@ extension SQLKit.SQLDataType: Swift.Equatable { } } } - -/// Used when testing defaulted async implementations -struct TestNoAsyncDatabase: SQLDatabase { - func execute(sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> ()) -> EventLoopFuture { self.eventLoop.makeSucceededVoidFuture() } - var logger: Logger { .init(label: "l") } - var eventLoop: any EventLoop { NIOAsyncTestingEventLoop() } - var dialect: any SQLDialect { GenericDialect() } -} From 6e7b3a919dfda924f3ea9da8b08a29f3d077ac0b Mon Sep 17 00:00:00 2001 From: Tim <0xtimc@gmail.com> Date: Wed, 5 Aug 2026 13:58:51 +0100 Subject: [PATCH 11/11] Tidy up old stuff --- Sources/SQLKit/Database/SQLDatabase.swift | 9 --------- Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md | 4 ---- 2 files changed, 13 deletions(-) diff --git a/Sources/SQLKit/Database/SQLDatabase.swift b/Sources/SQLKit/Database/SQLDatabase.swift index bcd0c4bd..0501577d 100644 --- a/Sources/SQLKit/Database/SQLDatabase.swift +++ b/Sources/SQLKit/Database/SQLDatabase.swift @@ -169,15 +169,6 @@ extension SQLDatabase { } extension SQLDatabase { - /// The default implementation for ``execute(sql:_:)-4eg19``. - @inlinable - public func execute( - sql query: any SQLExpression, - _ onRow: @escaping @Sendable (any SQLRow) -> () - ) async throws { - try await self.execute(sql: query, onRow) - } - /// The default implementation for ``withSession(_:)-9b68j``. @inlinable public func withSession( diff --git a/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md b/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md index c10763d1..e81d1184 100644 --- a/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md +++ b/Sources/SQLKit/Docs.docc/SQLDatabase+ExtensionDocs.md @@ -60,7 +60,3 @@ ### Logging - ``SQLDatabase/logging(to:)`` - -### Legacy query interface - -- ``SQLDatabase/execute(sql:_:)->_``