From e8dd94def3ccde3f8853e6a0d3a9a0bca10884b0 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 27 Jul 2026 17:08:32 -0600 Subject: [PATCH 1/3] Make the SwiftNIO and AsyncKit surface conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Neither SwiftNIO nor AsyncKit can be built for `wasm32-unknown-wasip1`: NIOPosix is built on POSIX sockets and threads, neither of which WASI preview 1 provides, and AsyncKit's connection pool is built on NIOPosix in turn. SQLiteKit uses them for the connection pool and for the legacy `EventLoopFuture` half of an API whose `async` half is already the recommended one, so it can build without them, given somewhere to put the differences. Modifications: Gate the affected declarations with `#if canImport(...)`, keyed on the module that actually supplies each API rather than on a platform, so the sources stay in step with whatever the manifest resolves for the target being built and with the gates sqlite-nio and sql-kit apply to the same dependencies. What drops out where the modules are absent: - `SQLiteConnectionSource` — a `ConnectionPoolSource` over AsyncKit and `NIOThreadPool` — drops out entirely, along with the `AsyncKit` re-export. Callers use sqlite-nio's concrete `async` `SQLiteConnection` directly; there is no connection pool on those platforms. - The `SQLDatabase` wrapper's `eventLoop`, its `execute(sql:_:) -> EventLoopFuture` overload, and `withSession(_:)` (which rides sqlite-nio's protocol-level `withConnection`) drop out. SQLKit removes the first two as protocol requirements on the same platforms and supplies a default for the third. - `SQLiteDataDecoder`'s JSON fallback reads a `[UInt8]` blob rather than a `ByteBuffer`, matching `SQLiteData`'s representation there. The decoder's two blob cases stay forked rather than collapsing to the `[UInt8]` form on both: `Data.init(buffer:byteTransferStrategy:)` is a zero-copy bridge with no `[UInt8]` equivalent, so collapsing them would silently add a copy where SwiftNIO is present. The gate names NIOFoundationCompat, which is the module that declares that initializer. `SQLiteDataEncoder`'s `import NIOCore` is dropped rather than gated: the file references no NIOCore symbol, so the import was already dead. Result: Where SwiftNIO and AsyncKit are available the public API and the symbol graph are unchanged — the same symbols with the same `docComment` line counts — and `diagnose-api-breaking-changes` reports no differences. Where they are absent SQLiteKit compiles down to the pool-free, `async`-only surface. --- Sources/SQLiteKit/Exports.swift | 3 +++ Sources/SQLiteKit/SQLiteConnection+SQLKit.swift | 10 ++++++++++ Sources/SQLiteKit/SQLiteConnectionSource.swift | 5 +++++ Sources/SQLiteKit/SQLiteDataDecoder.swift | 10 ++++++++++ Sources/SQLiteKit/SQLiteDataEncoder.swift | 1 - 5 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Sources/SQLiteKit/Exports.swift b/Sources/SQLiteKit/Exports.swift index 6656d7a..f4b1639 100644 --- a/Sources/SQLiteKit/Exports.swift +++ b/Sources/SQLiteKit/Exports.swift @@ -1,4 +1,7 @@ @_documentation(visibility: internal) @_exported import SQLKit @_documentation(visibility: internal) @_exported import SQLiteNIO +// AsyncKit, and with it the connection pool, is gated to the same platforms as SwiftNIO (see Package.swift). +#if canImport(AsyncKit) @_documentation(visibility: internal) @_exported import AsyncKit +#endif @_documentation(visibility: internal) @_exported import struct Logging.Logger diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift index 5bd69ce..19ed883 100644 --- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift +++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift @@ -173,11 +173,13 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { @usableFromInline let decoder: SQLiteDataDecoder + #if canImport(NIOCore) // See `SQLDatabase.eventLoop`. @usableFromInline var eventLoop: any EventLoop { self.database.eventLoop } + #endif // See `SQLDatabase.version`. @usableFromInline @@ -209,6 +211,9 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { self.queryLogLevel = queryLogLevel } + // The `EventLoopFuture` overload only exists where SwiftNIO does; SQLKit's protocol drops the + // requirement on the other platforms and the `async` overload below carries the whole surface. + #if canImport(NIOCore) // See `SQLDatabase.execute(sql:_:)`. @usableFromInline func execute( @@ -234,6 +239,7 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { { onRow($0.sql(decoder: self.decoder)) } ) } + #endif // See `SQLDatabase.execute(sql:_:)`. @usableFromInline @@ -254,6 +260,9 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { ) } + // `withSession(_:)` rides sqlite-nio's protocol-level `withConnection`, which exists only on the + // SwiftNIO build; elsewhere SQLKit's default `withSession` applies. + #if canImport(NIOCore) // See `SQLDatabase.withSession(_:)`. @usableFromInline func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { @@ -261,4 +270,5 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { try await closure($0.sql(encoder: self.encoder, decoder: self.decoder, queryLogLevel: self.queryLogLevel)) } } + #endif } diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift index e327a50..c3026de 100644 --- a/Sources/SQLiteKit/SQLiteConnectionSource.swift +++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift @@ -1,3 +1,7 @@ +// ``SQLiteConnectionSource`` is a `ConnectionPoolSource`, built on AsyncKit and SwiftNIO's thread +// pool. Neither is available on every platform SQLiteKit supports; where they are absent, callers +// use sqlite-nio's concrete async `SQLiteConnection` directly and there is no pool. +#if canImport(AsyncKit) #if canImport(Darwin) import Foundation #else @@ -97,3 +101,4 @@ fileprivate extension SQLiteConfiguration.Storage { } } } +#endif // canImport(AsyncKit) diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift index 68fe89a..03cabe0 100644 --- a/Sources/SQLiteKit/SQLiteDataDecoder.swift +++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift @@ -1,7 +1,9 @@ import Foundation import SQLiteNIO @_spi(CodableUtilities) import SQLKit +#if canImport(NIOFoundationCompat) import NIOFoundationCompat +#endif /// Translates `SQLiteData` values received from the database into `Decodable` values. /// @@ -51,7 +53,15 @@ public struct SQLiteDataDecoder: Sendable { switch data { case .text(let str): buf = .init(str.utf8) + // `Data.init(buffer:byteTransferStrategy:)` is NIOFoundationCompat's zero-copy + // bridge and has no `[UInt8]` equivalent. The two cases stay forked rather than + // collapsing to the `Data(blob)` form, which would silently add a copy on the + // SwiftNIO path. + #if canImport(NIOFoundationCompat) case .blob(let blob): buf = .init(buffer: blob, byteTransferStrategy: .noCopy) + #else + case .blob(let blob): buf = .init(blob) // `SQLiteData.blob` carries `[UInt8]` without SwiftNIO + #endif // The remaining cases should never happen, but we implement them anyway just in case. case .integer(let n): buf = .init(String(n).utf8) case .float(let n): buf = .init(String(n).utf8) diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift index 97e7ed3..b483eef 100644 --- a/Sources/SQLiteKit/SQLiteDataEncoder.swift +++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift @@ -1,4 +1,3 @@ -import NIOCore import Foundation @_spi(CodableUtilities) import SQLKit import SQLiteNIO From 0b96205f56a75fee088e5bef7ea5995d75efb1b4 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 27 Jul 2026 15:13:56 -0600 Subject: [PATCH 2/3] Elide SwiftNIO and AsyncKit on WASI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Neither SwiftNIO nor AsyncKit builds for `wasm32-unknown-wasip1`, so SQLiteKit currently fails to configure for that platform at all — the failure is in dependency resolution, before any of the conditional sources in the previous commit get a chance to matter. Modifications: Gate the NIOFoundationCompat and AsyncKit products on `.when(platforms: nonWASIPlatforms)` in both manifests. Target dependency conditions are evaluated per platform, so on WASI the products are simply not linked and the `canImport(...)` gates select the pool-free `async` API. `.when(platforms:)` can only include, never exclude, so excluding one platform means enumerating the others; each list is the set the manifest's own tools version knows about, noted as such so it is not extended without also raising that version. Result: On every other platform the resolved dependency set is byte-identical to before. On WASI the build graph contains no SwiftNIO and no AsyncKit module. --- Package.swift | 13 +++++++++++-- Package@swift-5.9.swift | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 3d1b537..a223716 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,11 @@ // swift-tools-version:5.8 import PackageDescription +/// This list matches the [supported platforms on the Swift 5.8 release of SPM](https://github.com/swiftlang/swift-package-manager/blob/release/5.8/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: "sqlite-kit", platforms: [ @@ -22,8 +27,12 @@ let package = Package( .target( name: "SQLiteKit", dependencies: [ - .product(name: "NIOFoundationCompat", package: "swift-nio"), - .product(name: "AsyncKit", package: "async-kit"), + // Neither SwiftNIO nor AsyncKit builds for wasm32-unknown-wasip1. Target dependency + // conditions are evaluated per platform, so on WASI these are simply not linked; + // SQLiteKit then compiles without the connection pool and without the + // EventLoopFuture surface (see the `#if canImport(...)` gates in Sources/). + .product(name: "NIOFoundationCompat", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), + .product(name: "AsyncKit", package: "async-kit", condition: .when(platforms: nonWASIPlatforms)), .product(name: "SQLiteNIO", package: "sqlite-nio"), .product(name: "SQLKit", package: "sql-kit"), ], diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index ae4fbb4..1ca2fbc 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -1,6 +1,11 @@ // swift-tools-version:5.9 import PackageDescription +/// This list matches the [supported platforms on the Swift 5.9 release of SPM](https://github.com/swiftlang/swift-package-manager/blob/release/5.9/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: "sqlite-kit", platforms: [ @@ -22,8 +27,12 @@ let package = Package( .target( name: "SQLiteKit", dependencies: [ - .product(name: "NIOFoundationCompat", package: "swift-nio"), - .product(name: "AsyncKit", package: "async-kit"), + // Neither SwiftNIO nor AsyncKit builds for wasm32-unknown-wasip1. Target dependency + // conditions are evaluated per platform, so on WASI these are simply not linked; + // SQLiteKit then compiles without the connection pool and without the + // EventLoopFuture surface (see the `#if canImport(...)` gates in Sources/). + .product(name: "NIOFoundationCompat", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), + .product(name: "AsyncKit", package: "async-kit", condition: .when(platforms: nonWASIPlatforms)), .product(name: "SQLiteNIO", package: "sqlite-nio"), .product(name: "SQLKit", package: "sql-kit"), ], From f0bff698f056fdcfdd191c817f1ba680a582d76e Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 27 Jul 2026 17:08:45 -0600 Subject: [PATCH 3/3] Build for WebAssembly in CI Motivation: `vapor/ci`'s reusable unit-test workflow already knows how to build a package for `wasm32-unknown-wasip1`; sqlite-kit simply had not opted in, so nothing stops the SwiftNIO-free configuration from regressing unnoticed. Modifications: Pass `with_wasm: true` to the reusable workflow, as sql-kit already does. Result: The WASI build is checked on every pull request. --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 27ce87c..995942f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,8 @@ jobs: contents: read uses: vapor/ci/.github/workflows/run-unit-tests.yml@main secrets: inherit + with: + with_wasm: true # Make sure downstream dependents still work dependents-check: