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: diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index ae4fbb4..90120ad 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -1,6 +1,9 @@ // swift-tools-version:5.9 import PackageDescription +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: [ @@ -13,16 +16,21 @@ let package = Package( .library(name: "SQLiteKit", targets: ["SQLiteKit"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), - .package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.9.0"), - .package(url: "https://github.com/vapor/sql-kit.git", from: "3.29.3"), - .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), + // WASM: upstream swift-nio + standalone nio-async-runtime; wasm-safe vapor forks. + // Pin to tagged fork releases once available. + .package(url: "https://github.com/apple/swift-nio.git", from: "2.89.0"), + .package(url: "https://github.com/PassiveLogic/nio-async-runtime.git", from: "1.0.0"), + .package(url: "https://github.com/PassiveLogic/sqlite-nio.git", from: "1.9.0"), + .package(url: "https://github.com/vapor/sql-kit.git", from: "3.33.1"), + .package(url: "https://github.com/PassiveLogic/async-kit.git", from: "1.19.0"), ], targets: [ .target( name: "SQLiteKit", dependencies: [ .product(name: "NIOFoundationCompat", package: "swift-nio"), + .product(name: "NIOAsyncRuntime", package: "nio-async-runtime", condition: .when(platforms: [.wasi])), + .product(name: "NIOPosix", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), .product(name: "AsyncKit", package: "async-kit"), .product(name: "SQLiteNIO", package: "sqlite-nio"), .product(name: "SQLKit", package: "sql-kit"), diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift index e327a50..dbffeae 100644 --- a/Sources/SQLiteKit/SQLiteConnectionSource.swift +++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift @@ -5,7 +5,20 @@ import Foundation #endif import Logging import AsyncKit +// Use NIOPosix on every host platform and substitute NIOAsyncRuntime on +// WASI. Static `os(WASI)` is the right gate: +// - `canImport(NIOAsyncRuntime)` matches on macOS / Linux too when the +// module is in the dep graph as a transitive, but its types are +// `@available(macOS 15, *)` and break consumers with older deployment +// targets. +// - `canImport(NIOPosix)` matches on WASI as a partial-module stub that +// doesn't actually expose `NIOThreadPool` there, so the import +// link-fails. +#if os(WASI) +import NIOAsyncRuntime +#else import NIOPosix +#endif import SQLiteNIO import NIOCore @@ -16,7 +29,13 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { private let threadPool: NIOThreadPool private var connectionStorage: SQLiteConnection.Storage { + #if os(WASI) + // NOTE: For WASI platforms, file urls and paths cause runtime errors currently. Using + // in-memory connection only as a workaround. + .memory + #else .file(path: self.actualURL.absoluteString) + #endif } /// Create a new ``SQLiteConnectionSource``. diff --git a/Tests/SQLiteKitTests/SQLiteKitTests.swift b/Tests/SQLiteKitTests/SQLiteKitTests.swift index 599a9f0..76b1acd 100644 --- a/Tests/SQLiteKitTests/SQLiteKitTests.swift +++ b/Tests/SQLiteKitTests/SQLiteKitTests.swift @@ -105,10 +105,11 @@ final class SQLiteKitTests: XCTestCase { ) let conn2 = try await source.makeConnection(logger: self.connection.logger, on: MultiThreadedEventLoopGroup.singleton.any()).get() - defer { try! conn2.close().wait() } let res2 = try await conn2.query("PRAGMA foreign_keys").get() XCTAssertEqual(res2[0].column("foreign_keys"), .integer(0)) + + try! await conn2.close().get() } func testJSONStringColumn() async throws { @@ -123,6 +124,11 @@ final class SQLiteKitTests: XCTestCase { XCTAssertEqual(bar.baz, "qux") } + // NOTE: The following test doesn't work in a runtime environment + // due to reliance on temp files. The test is elided for now + // for WASI targets, but could be used in the future once + // a persistence solution is working for WASI platforms. + #if !os(WASI) func testMultipleInMemoryDatabases() async throws { let a = SQLiteConnectionSource( configuration: .init(storage: .memory, enableForeignKeys: true), @@ -134,19 +140,21 @@ final class SQLiteKitTests: XCTestCase { ) let a1 = try await a.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() - defer { try! a1.close().wait() } let a2 = try await a.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() - defer { try! a2.close().wait() } let b1 = try await b.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() - defer { try! b1.close().wait() } let b2 = try await b.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get() - defer { try! b2.close().wait() } _ = try await a1.query("CREATE TABLE foo (bar INTEGER)").get() _ = try await a2.query("SELECT * FROM foo").get() _ = try await b1.query("CREATE TABLE foo (bar INTEGER)").get() _ = try await b2.query("SELECT * FROM foo").get() + + try! await b2.close().get() + try! await b1.close().get() + try! await a2.close().get() + try! await a1.close().get() } + #endif // !os(WASI) // https://github.com/vapor/sqlite-kit/issues/56 func testDoubleConstraintError() async throws {