From 70d63e49ef2a0a09ec7f7d45a381a94e93cf235c Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 27 Jul 2026 17:07:00 -0600 Subject: [PATCH 1/2] feat: make the `EventLoopFuture` surface conditional on SwiftNIO Motivation: The SQL drivers beneath SQLKit cannot build SwiftNIO for `wasm32-unknown-wasip1`: NIOPosix is built on POSIX sockets and threads, neither of which WASI preview 1 provides. SQLKit itself needs very little of SwiftNIO, only `EventLoop` and `EventLoopFuture`, and only for the legacy half of an API whose `async` half is already the recommended one, so it can build without it, given somewhere to put the differences. The goal is one implementation with a few conditional declarations, not a second copy of the package that has to be kept in step by hand. Modifications: Gate the legacy `EventLoopFuture` surface with `#if canImport(NIOCore)`. Keying on `canImport` rather than on a platform keeps the sources in step with whatever the manifest resolves for the target being built, and makes the gate unconditionally true wherever SwiftNIO is present. What drops out where it is absent: the `eventLoop` property, the `execute(sql:_:) -> EventLoopFuture` requirement and its default bridge to `async`, the `EventLoopFuture` variants of `SQLQueryBuilder.run()` and of the `SQLQueryFetcher` `first`/`all`/`run` families, the NIOCore re-exports, and the deprecated `SQLBenchmarker` future bridges. Each `#if` encloses the doc comment of the declaration it gates rather than sitting between the two. A `#if` in that position detaches the comment: the declaration still compiles, but the symbol graph reports it as undocumented, so the published API docs lose the entry with no build-time diagnostic. Placed correctly, the doc comments are byte-identical to before. Result: Wherever SwiftNIO is available the public API, the documentation and the symbol graph are unchanged. `swift build --target SQLKit -Xswiftc -emit-symbol-graph` emits the same symbols with the same `docComment` line counts as before, and `diagnose-api-breaking-changes` reports no differences. Where it is absent the `async` surface, already the recommended API on every one of those calls, is unaffected and becomes the only one. --- .../Builders/Prototypes/SQLQueryBuilder.swift | 8 ++++++- .../Builders/Prototypes/SQLQueryFetcher.swift | 13 +++++++++++ Sources/SQLKit/Database/SQLDatabase.swift | 23 +++++++++++++++++-- Sources/SQLKit/Exports.swift | 2 ++ Sources/SQLKitBenchmark/SQLBenchmarker.swift | 5 ++++ 5 files changed, 48 insertions(+), 3 deletions(-) diff --git a/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift b/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift index eb258737..c17b1e22 100644 --- a/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift +++ b/Sources/SQLKit/Builders/Prototypes/SQLQueryBuilder.swift @@ -1,4 +1,6 @@ +#if canImport(NIOCore) import class NIOCore.EventLoopFuture +#endif /// Base definitions for builders which set up queries and execute them against a given database. /// @@ -10,18 +12,21 @@ public protocol SQLQueryBuilder: AnyObject { /// Connection to execute query on. var database: any SQLDatabase { get } + #if canImport(NIOCore) /// 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 - + #endif + /// Execute the query on the connection, ignoring any results. func run() async throws } extension SQLQueryBuilder { + #if canImport(NIOCore) /// 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. @@ -29,6 +34,7 @@ extension SQLQueryBuilder { public func run() -> EventLoopFuture { self.database.execute(sql: self.query) { _ in } } + #endif // canImport(NIOCore) /// Execute the query associated with the builder on the builder's database, ignoring any results. /// diff --git a/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift b/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift index f6c9489c..a8066bfd 100644 --- a/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift +++ b/Sources/SQLKit/Builders/Prototypes/SQLQueryFetcher.swift @@ -1,10 +1,15 @@ +// `EventLoopFuture` is unavailable where SwiftNIO is not linked. The `async` `first()`/`all()`/ +// `run(_:)` families further down are unconditional and carry the whole surface there. +#if canImport(NIOCore) import class NIOCore.EventLoopFuture +#endif /// Common definitions for ``SQLQueryBuilder``s which support retrieving result rows. public protocol SQLQueryFetcher: SQLQueryBuilder {} // MARK: - First (EventLoopFuture) +#if canImport(NIOCore) extension SQLQueryFetcher { /// Returns the named column from the first output row, if any, decoded as a given type. /// @@ -70,6 +75,8 @@ extension SQLQueryFetcher { } } +#endif // canImport(NIOCore) + // MARK: - First (async) extension SQLQueryFetcher { @@ -144,6 +151,7 @@ extension SQLQueryFetcher { // MARK: - All (EventLoopFuture) +#if canImport(NIOCore) extension SQLQueryFetcher { /// Returns the named column from each output row, if any, decoded as a given type. /// @@ -205,6 +213,8 @@ extension SQLQueryFetcher { } } +#endif // canImport(NIOCore) + // MARK: - All (async) extension SQLQueryFetcher { @@ -271,6 +281,7 @@ extension SQLQueryFetcher { // MARK: - Run (EventLoopFuture) +#if canImport(NIOCore) 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. @@ -337,6 +348,8 @@ extension SQLQueryFetcher { } } +#endif // canImport(NIOCore) + // MARK: - Run (async) extension SQLQueryFetcher { diff --git a/Sources/SQLKit/Database/SQLDatabase.swift b/Sources/SQLKit/Database/SQLDatabase.swift index 376186c4..429a7dc9 100644 --- a/Sources/SQLKit/Database/SQLDatabase.swift +++ b/Sources/SQLKit/Database/SQLDatabase.swift @@ -1,5 +1,9 @@ +// SwiftNIO is not linked on every platform SQLKit supports (see Package.swift); the +// EventLoopFuture surface below is elided where it is absent. +#if canImport(NIOCore) import protocol NIOCore.EventLoop import class NIOCore.EventLoopFuture +#endif import struct Logging.Logger /// The common interface to SQLKit for both drivers and client code. @@ -56,6 +60,9 @@ public protocol SQLDatabase: Sendable { /// The `Logger` used for logging all operations relating to a given database. var logger: Logger { get } + // N.B.: Each `#if` encloses the doc comment of the declaration it gates; placed between the + // two it would silently detach the doc from the symbol graph. + #if canImport(NIOCore) /// 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 @@ -63,7 +70,8 @@ public protocol SQLDatabase: Sendable { /// 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 } - + #endif // canImport(NIOCore) + /// The version number the database reports for itself. /// /// The version must be provided via a type conforming to the ``SQLDatabaseReportedVersion`` protocol. If the @@ -102,6 +110,7 @@ public protocol SQLDatabase: Sendable { /// > it's unavoidable, as there are no direct entry points to SQLKit without a driver. var queryLogLevel: Logger.Level? { get } + #if canImport(NIOCore) /// 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). /// @@ -118,6 +127,7 @@ public protocol SQLDatabase: Sendable { sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () ) -> EventLoopFuture + #endif // canImport(NIOCore) /// 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). @@ -200,6 +210,10 @@ extension SQLDatabase { } extension SQLDatabase { + // This default bridges the `async` requirement to the legacy `EventLoopFuture` one, so it can + // only exist where the latter does. Without SwiftNIO there is no future overload to forward to + // and conformers implement the `async` `execute` directly. + #if canImport(NIOCore) /// The default implementation for ``execute(sql:_:)-4eg19``. @inlinable public func execute( @@ -208,7 +222,8 @@ extension SQLDatabase { ) async throws { try await self.execute(sql: query, onRow).get() } - + #endif // canImport(NIOCore) + /// The default implementation for ``withSession(_:)-9b68j``. @inlinable public func withSession( @@ -227,10 +242,12 @@ private struct CustomLoggerSQLDatabase: SQLDatabase { // See `SQLDatabase.logger`. let logger: Logger + #if canImport(NIOCore) // See `SQLDatabase.eventLoop`. var eventLoop: any EventLoop { self.database.eventLoop } + #endif // See `SQLDatabase.version`. var version: (any SQLDatabaseReportedVersion)? { @@ -247,6 +264,7 @@ private struct CustomLoggerSQLDatabase: SQLDatabase { self.database.queryLogLevel } + #if canImport(NIOCore) // See `SQLDatabase.execute(sql:_:)`. func execute( sql query: any SQLExpression, @@ -254,6 +272,7 @@ private struct CustomLoggerSQLDatabase: SQLDatabase { ) -> EventLoopFuture { self.database.execute(sql: query, onRow) } + #endif // canImport(NIOCore) // See `SQLDatabase.execute(sql:_:)`. func execute( diff --git a/Sources/SQLKit/Exports.swift b/Sources/SQLKit/Exports.swift index 370dd880..d9841707 100644 --- a/Sources/SQLKit/Exports.swift +++ b/Sources/SQLKit/Exports.swift @@ -1,3 +1,5 @@ +#if canImport(NIOCore) @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop @_documentation(visibility: internal) @_exported import class NIOCore.EventLoopFuture +#endif @_documentation(visibility: internal) @_exported import struct Logging.Logger diff --git a/Sources/SQLKitBenchmark/SQLBenchmarker.swift b/Sources/SQLKitBenchmark/SQLBenchmarker.swift index 964de1dc..865b13c3 100644 --- a/Sources/SQLKitBenchmark/SQLBenchmarker.swift +++ b/Sources/SQLKitBenchmark/SQLBenchmarker.swift @@ -1,5 +1,7 @@ import Logging +#if canImport(NIOCore) import NIOCore +#endif public import SQLKit import XCTest @@ -21,6 +23,8 @@ public final class SQLBenchmarker: Sendable { } } + // The deprecated EventLoopFuture bridges are elided where SwiftNIO is unavailable. + #if canImport(NIOCore) @available(*, deprecated, renamed: "runAllTests()", message: "Use `runAllTests()` instead.") public func testAll() throws { try database.eventLoop.makeFutureWithTask { try await self.runAllTests() }.wait() @@ -30,6 +34,7 @@ public final class SQLBenchmarker: Sendable { public func run() throws { try self.testAll() } + #endif // canImport(NIOCore) func runTest( _ name: String = #function, From 68c28ac2d186fd258a738682659353b29122bc08 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 27 Jul 2026 15:06:36 -0600 Subject: [PATCH 2/2] build: elide SwiftNIO on WASI Motivation: Since vapor/sql-kit#190 and swift-nio's WASI support, SQLKit itself already builds for `wasm32-unknown-wasip1` with NIOCore linked. The drivers beneath it do not: NIOPosix needs the POSIX sockets and threads WASI preview 1 lacks, so a driver on that platform has no SwiftNIO at all, and a SQLKit that kept its `EventLoopFuture` surface there would impose requirements no driver could implement. Gating NIOCore out keeps `canImport(NIOCore)` uniformly false across the stack on WASI, so one condition selects the async-only configuration everywhere. Modifications: Gate the NIOCore product on `.when(platforms: nonWASIPlatforms)`. Target dependency conditions are evaluated per platform, so on WASI the product is simply not linked and the `canImport(NIOCore)` gates select the `async` API. `.when(platforms:)` can only include, never exclude, so excluding one platform means enumerating the others; the list is the set SPM 6.1 knows about, noted as such so it is not extended without also raising the manifest's tools version. The test target's NIOCore and NIOEmbedded dependencies are gated the same way, as are the suite's imports of them. The suite exercises the `EventLoopFuture` API and is not run on WASI, and `swift build` evaluates `--explicit-target-dependency-import-check` for every target in the graph, including the test target it does not build. Result: On every other platform the resolved dependency set is byte-identical to before. On WASI the build graph contains no SwiftNIO module: not NIOPosix, not NIOCore, not NIOConcurrencyHelpers. --- Package.swift | 19 ++++++++++++++++--- Tests/SQLKitTests/AsyncTests.swift | 2 ++ Tests/SQLKitTests/BaseTests.swift | 2 ++ Tests/SQLKitTests/TestMocks.swift | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 48cc9bef..76a4e339 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,12 @@ // swift-tools-version:6.1 import PackageDescription +/// `.when(platforms:)` can only include, never exclude, so excluding WASI means listing everything else. +/// This list matches the [supported platforms on the Swift 6.1 release of SPM](https://github.com/swiftlang/swift-package-manager/blob/release/6.1/Sources/PackageDescription/SupportedPlatforms.swift). +/// Don't add new platforms here unless raising the swift-tools-version of this manifest. +let allPlatforms: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS, .driverKit, .linux, .windows, .android, .wasi, .openbsd] +let nonWASIPlatforms: [Platform] = allPlatforms.filter { $0 != .wasi } + let package = Package( name: "sql-kit", platforms: [ @@ -24,7 +30,13 @@ let package = Package( dependencies: [ .product(name: "Collections", package: "swift-collections"), .product(name: "Logging", package: "swift-log"), - .product(name: "NIOCore", package: "swift-nio"), + // NIOCore itself builds for wasm32-unknown-wasip1, but the drivers beneath SQLKit + // cannot build SwiftNIO there (NIOPosix needs POSIX sockets and threads), so no + // driver on that platform could implement an `EventLoopFuture` surface. Target + // dependency conditions are evaluated per platform, so on WASI NIOCore is simply + // not linked and that surface drops out via `#if canImport(NIOCore)`; the async + // surface is unaffected. + .product(name: "NIOCore", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), ], swiftSettings: swiftSettings ), @@ -38,8 +50,9 @@ let package = Package( .testTarget( name: "SQLKitTests", dependencies: [ - .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOEmbedded", package: "swift-nio"), + // The test suite exercises the SwiftNIO surface, so it is not built for WASI. + .product(name: "NIOCore", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), + .product(name: "NIOEmbedded", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), .target(name: "SQLKit"), .target(name: "SQLKitBenchmark"), ], diff --git a/Tests/SQLKitTests/AsyncTests.swift b/Tests/SQLKitTests/AsyncTests.swift index 3031b1ed..c09e065a 100644 --- a/Tests/SQLKitTests/AsyncTests.swift +++ b/Tests/SQLKitTests/AsyncTests.swift @@ -1,4 +1,6 @@ +#if canImport(NIOCore) import NIOCore +#endif import OrderedCollections import SQLKit import Testing diff --git a/Tests/SQLKitTests/BaseTests.swift b/Tests/SQLKitTests/BaseTests.swift index d9da5d6f..f4653075 100644 --- a/Tests/SQLKitTests/BaseTests.swift +++ b/Tests/SQLKitTests/BaseTests.swift @@ -1,7 +1,9 @@ @testable import SQLKit import struct Logging.Logger +#if canImport(NIOCore) import protocol NIOCore.EventLoop import class NIOCore.EventLoopFuture +#endif import SQLKitBenchmark import Testing diff --git a/Tests/SQLKitTests/TestMocks.swift b/Tests/SQLKitTests/TestMocks.swift index 85d3a12d..cba8ffc4 100644 --- a/Tests/SQLKitTests/TestMocks.swift +++ b/Tests/SQLKitTests/TestMocks.swift @@ -1,8 +1,10 @@ import OrderedCollections public import SQLKit +#if canImport(NIOCore) import protocol NIOCore.EventLoop import class NIOCore.EventLoopFuture import class NIOEmbedded.NIOAsyncTestingEventLoop +#endif import Logging #if canImport(Dispatch) import class Dispatch.DispatchQueue