From 312bd208487eddfbaf807442cc3f4dc76b13a9fe Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 5 May 2025 14:52:07 -0600 Subject: [PATCH] feat: Enable sqlite-nio to compile to wasm targets. --- .github/workflows/test.yml | 1 + Package.swift | 18 +++++++++-- Sources/SQLiteNIO/Exports.swift | 11 +++++++ Sources/SQLiteNIO/SQLiteConnection.swift | 5 ++++ Sources/SQLiteNIO/SQLiteData.swift | 30 ++++++++++++------- Sources/SQLiteNIO/SQLiteDatabase.swift | 1 - Sources/SQLiteNIO/SQLiteStatement.swift | 2 +- .../SQLiteCustomFunctionTests.swift | 2 +- Tests/SQLiteNIOTests/SQLiteNIOTests.swift | 8 ++--- 9 files changed, 57 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac7a45f..5ff3d44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,3 +39,4 @@ jobs: with: with_musl: true with_android: true + with_wasm: true diff --git a/Package.swift b/Package.swift index e8b1eb7..f4b3d9b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,12 @@ // swift-tools-version:5.10 import PackageDescription +/// This list matches the [supported platforms on the Swift 5.10 release of SPM](https://github.com/swiftlang/swift-package-manager/blob/release/5.10/Sources/PackageDescription/SupportedPlatforms.swift#L34-L71) +/// 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 wasiPlatform: [Platform] = [.wasi] + let package = Package( name: "sqlite-nio", platforms: [ @@ -13,7 +19,9 @@ let package = Package( .library(name: "SQLiteNIO", targets: ["SQLiteNIO"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + // TODO: SM: Update swift-nio version once NIOAsyncRuntime is available from swift-nio + // .package(url: "https://github.com/apple/swift-nio.git", from: "2.89.0"), + .package(url: "https://github.com/PassiveLogic/swift-nio.git", branch: "feat/addNIOAsyncRuntimeForWasm"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), ], targets: [ @@ -38,6 +46,7 @@ let package = Package( .target(name: "CSQLite"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOAsyncRuntime", package: "swift-nio", condition: .when(platforms: wasiPlatform)), .product(name: "NIOPosix", package: "swift-nio"), .product(name: "NIOFoundationCompat", package: "swift-nio"), ], @@ -92,7 +101,12 @@ var sqliteCSettings: [CSetting] { [ .define("SQLITE_OMIT_TCL_VARIABLE"), .define("SQLITE_OMIT_TRACE"), .define("SQLITE_SECURE_DELETE"), - .define("SQLITE_THREADSAFE", to: "1"), + .define("SQLITE_THREADSAFE", to: "1", .when(platforms: nonWASIPlatforms)), + // For now, we use the single threaded sqlite variation for the WASI platform + // since single-threaded operation is the least common denominator capability + // for Wasm executables and it is considered unreliable to use canImport(wasi_pthread) + // in a manifest file to distinguish between the two capabilities. + .define("SQLITE_THREADSAFE", to: "0", .when(platforms: wasiPlatform)), .define("SQLITE_UNTESTABLE"), .define("SQLITE_USE_URI"), ] } diff --git a/Sources/SQLiteNIO/Exports.swift b/Sources/SQLiteNIO/Exports.swift index 0542622..38a32e8 100644 --- a/Sources/SQLiteNIO/Exports.swift +++ b/Sources/SQLiteNIO/Exports.swift @@ -1,5 +1,16 @@ @_documentation(visibility: internal) @_exported import struct NIOCore.ByteBuffer + +#if canImport(NIOAsyncRuntime) +@_documentation(visibility: internal) @_exported import class NIOAsyncRuntime.AsyncThreadPool +#elseif canImport(NIOPosix) @_documentation(visibility: internal) @_exported import class NIOPosix.NIOThreadPool +#endif + @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup + +#if canImport(NIOAsyncRuntime) +@_documentation(visibility: internal) @_exported import class NIOAsyncRuntime.AsyncEventLoopGroup +#elseif canImport(NIOPosix) @_documentation(visibility: internal) @_exported import class NIOPosix.MultiThreadedEventLoopGroup +#endif diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift index a728806..dbd0f28 100644 --- a/Sources/SQLiteNIO/SQLiteConnection.swift +++ b/Sources/SQLiteNIO/SQLiteConnection.swift @@ -1,5 +1,10 @@ import NIOConcurrencyHelpers import NIOCore +#if canImport(NIOAsyncRuntime) +import NIOAsyncRuntime +typealias MultiThreadedEventLoopGroup = AsyncEventLoopGroup +public typealias NIOThreadPool = AsyncThreadPool +#endif import NIOPosix import CSQLite import Logging diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift index adf7588..8455a15 100644 --- a/Sources/SQLiteNIO/SQLiteData.swift +++ b/Sources/SQLiteNIO/SQLiteData.swift @@ -1,13 +1,25 @@ import CSQLite import NIOCore +#if _pointerBitWidth(_64) +/// We use `Int` on 64-bit systems due to public API breakage concerns. +public typealias SQLiteInt64 = Int // 64-bit platform, Int = 64 bits +#elseif _pointerBitWidth(_32) +public typealias SQLiteInt64 = Int64 // On 32-bit platforms, we want to use 64 bit integers. +#else +/// If you hit errors here, you may simply need to add a new architectural bit size above (e.g. _128) +/// when that exists. Or, if [this proposal for pointerBitWidth](https://forums.swift.org/t/pitch-pointer-bit-width-compile-time-conditional/59572) +/// ever lands in a published Swift version, then the above conditionals may need to be adjusted: +#error("Unsupported integer size") +#endif + /// Encapsulates a single data item provided by or to SQLite. /// /// SQLite supports four data type "affinities" - INTEGER, REAL, TEXT, and BLOB - plus the `NULL` value, which has no /// innate affinity. public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable { /// `INTEGER` affinity, represented in Swift by `Int`. - case integer(Int) + case integer(SQLiteInt64) /// `REAL` affinity, represented in Swift by `Double`. case float(Double) @@ -25,16 +37,12 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable /// /// If the data has `REAL` or `TEXT` affinity, an attempt is made to interpret the value as an integer. `BLOB` /// and `NULL` values always return `nil`. - public var integer: Int? { + public var integer: SQLiteInt64? { switch self { - case .integer(let integer): - return integer - case .float(let double): - return Int(double) - case .text(let string): - return Int(string) - case .blob, .null: - return nil + case .integer(let integer): integer + case .float(let double): .init(double) + case .text(let string): .init(string) + case .blob, .null: nil } } @@ -137,7 +145,7 @@ extension SQLiteData { case SQLITE_NULL: self = .null case SQLITE_INTEGER: - self = .integer(Int(sqlite_nio_sqlite3_value_int64(sqliteValue))) + self = .integer(.init(sqlite_nio_sqlite3_value_int64(sqliteValue))) case SQLITE_FLOAT: self = .float(sqlite_nio_sqlite3_value_double(sqliteValue)) case SQLITE_TEXT: diff --git a/Sources/SQLiteNIO/SQLiteDatabase.swift b/Sources/SQLiteNIO/SQLiteDatabase.swift index e236822..8fc3518 100644 --- a/Sources/SQLiteNIO/SQLiteDatabase.swift +++ b/Sources/SQLiteNIO/SQLiteDatabase.swift @@ -1,5 +1,4 @@ import NIOCore -import NIOPosix import CSQLite import Logging diff --git a/Sources/SQLiteNIO/SQLiteStatement.swift b/Sources/SQLiteNIO/SQLiteStatement.swift index e32c897..21912b4 100644 --- a/Sources/SQLiteNIO/SQLiteStatement.swift +++ b/Sources/SQLiteNIO/SQLiteStatement.swift @@ -92,7 +92,7 @@ struct SQLiteStatement { private func data(at offset: Int32) throws -> SQLiteData { switch sqlite_nio_sqlite3_column_type(self.handle, offset) { case SQLITE_INTEGER: - return .integer(Int(sqlite_nio_sqlite3_column_int64(self.handle, offset))) + return .integer(.init(sqlite_nio_sqlite3_column_int64(self.handle, offset))) case SQLITE_FLOAT: return .float(Double(sqlite_nio_sqlite3_column_double(self.handle, offset))) case SQLITE_TEXT: diff --git a/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift b/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift index 0ca82bf..24dadc9 100644 --- a/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift +++ b/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift @@ -46,7 +46,7 @@ final class DatabaseFunctionTests: XCTestCase { try await withOpenedConnection { conn in let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in 1 } try await conn.install(customFunction: fn) - await XCTAssertEqualAsync(Int(1), try await conn.query("SELECT f() as result").first?.column("result")?.integer) + await XCTAssertEqualAsync(.init(1), try await conn.query("SELECT f() as result").first?.column("result")?.integer) } } diff --git a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift index 602ef90..6419ff5 100644 --- a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift +++ b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift @@ -1,8 +1,6 @@ import XCTest import SQLiteNIO import Logging -import NIOCore -import NIOPosix import NIOFoundationCompat /// Run the provided closure with an opened ``SQLiteConnection`` using an in-memory database and the singleton thread @@ -100,7 +98,7 @@ final class SQLiteNIOTests: XCTestCase { _ = try await conn.query(#"INSERT INTO test (date) VALUES (?)"#, [date.sqliteData!]) let rows = try await conn.query("SELECT * FROM test") - XCTAssertTrue(rows.first?.column("date") == .float(date.timeIntervalSince1970) || rows.first?.column("date") == .integer(Int(date.timeIntervalSince1970))) + XCTAssertTrue(rows.first?.column("date") == .float(date.timeIntervalSince1970) || rows.first?.column("date") == .integer(.init(date.timeIntervalSince1970))) XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.description, date.description) } } @@ -109,7 +107,7 @@ final class SQLiteNIOTests: XCTestCase { try await withOpenedConnection { conn in let rows = try await conn.query("SELECT 1 as foo, 2 as foo") let row0 = try XCTUnwrap(rows.first) - var i = 0 + var i: SQLiteInt64 = 0 for column in row0.columns { XCTAssertEqual(column.name, "foo") i += column.data.integer ?? 0 @@ -127,7 +125,7 @@ final class SQLiteNIOTests: XCTestCase { _ = try await conn.query(#"INSERT INTO scores (score) VALUES (?), (?), (?)"#, [.integer(3), .integer(4), .integer(5)]) struct MyAggregate: SQLiteCustomAggregate { - var sum: Int = 0 + var sum: SQLiteInt64 = 0 mutating func step(_ values: [SQLiteData]) throws { self.sum += (values.first?.integer ?? 0) }