From bf7b024fe7de579a1ce320eee81231137e4ce96b Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 5 May 2025 14:52:07 -0600 Subject: [PATCH 1/6] feat: Enable sqlite-nio to compile to wasm targets. --- .github/workflows/test.yml | 1 + Package.swift | 34 +++++++++++++++++++++-- Sources/SQLiteNIO/Exports.swift | 11 ++++++++ Sources/SQLiteNIO/SQLiteConnection.swift | 4 +++ Sources/SQLiteNIO/SQLiteData.swift | 29 +++++++++++++++---- Sources/SQLiteNIO/SQLiteDatabase.swift | 1 - Sources/SQLiteNIO/SQLiteStatement.swift | 2 +- Tests/SQLiteNIOTests/SQLiteNIOTests.swift | 2 -- 8 files changed, 73 insertions(+), 11 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..30edc85 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,9 @@ // swift-tools-version:5.10 import PackageDescription +let wasiPlatform: [Platform] = [.wasi] +let nonWASIPlatforms: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS, .driverKit, .linux, .windows, .android, .openbsd] + let package = Package( name: "sqlite-nio", platforms: [ @@ -13,7 +16,8 @@ let package = Package( .library(name: "SQLiteNIO", targets: ["SQLiteNIO"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.89.0"), + .package(url: "https://github.com/PassiveLogic/nio-async-runtime.git", from: "0.0.2"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), ], targets: [ @@ -38,7 +42,8 @@ let package = Package( .target(name: "CSQLite"), .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), - .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOAsyncRuntime", package: "nio-async-runtime", condition: .when(platforms: wasiPlatform)), + .product(name: "NIOPosix", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)), .product(name: "NIOFoundationCompat", package: "swift-nio"), ], swiftSettings: swiftSettings @@ -61,6 +66,30 @@ var swiftSettings: [SwiftSetting] { [ .enableExperimentalFeature("StrictConcurrency=complete"), ] } +#if canImport(WASILibc) +#if canImport(wasi_pthread) +// Some wasm SDKs provide pthread +// API's, and some future SDKs will as well. +// +// If pthread API's are available, we should use them. +// +// But if not, then we can't use them. +var threadSafetyConfiguration: CSetting { .define("SQLITE_THREADSAFE", to: "1") } +#else +// In this case, we're using a Swift for WebAssembly +// SDK that does NOT have pthread support, so we +// can't compile in thread safety. +// +// Note that by the same principal, the executable will +// almost certainly be single-threaded, so it is acceptable +// and fine to elide thread safety. But in this case, there +// isn't any other option. +var threadSafetyConfiguration: CSetting { .define("SQLITE_THREADSAFE", to: "0") } +#endif +#else +var threadSafetyConfiguration: CSetting { .define("SQLITE_THREADSAFE", to: "1") } +#endif + var sqliteCSettings: [CSetting] { [ // Derived from sqlite3 version 3.43.0 .define("SQLITE_DEFAULT_MEMSTATUS", to: "0"), @@ -92,6 +121,7 @@ var sqliteCSettings: [CSetting] { [ .define("SQLITE_OMIT_TCL_VARIABLE"), .define("SQLITE_OMIT_TRACE"), .define("SQLITE_SECURE_DELETE"), + threadSafetyConfiguration, .define("SQLITE_THREADSAFE", to: "1"), .define("SQLITE_UNTESTABLE"), .define("SQLITE_USE_URI"), diff --git a/Sources/SQLiteNIO/Exports.swift b/Sources/SQLiteNIO/Exports.swift index 0542622..b7d59df 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(NIOPosix) @_documentation(visibility: internal) @_exported import class NIOPosix.NIOThreadPool +#elseif canImport(NIOAsyncRuntime) +@_documentation(visibility: internal) @_exported import class NIOAsyncRuntime.NIOThreadPool +#endif + @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup + +#if canImport(NIOPosix) @_documentation(visibility: internal) @_exported import class NIOPosix.MultiThreadedEventLoopGroup +#elseif canImport(NIOAsyncRuntime) +@_documentation(visibility: internal) @_exported import class NIOAsyncRuntime.MultiThreadedEventLoopGroup +#endif diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift index a728806..9cb6598 100644 --- a/Sources/SQLiteNIO/SQLiteConnection.swift +++ b/Sources/SQLiteNIO/SQLiteConnection.swift @@ -1,6 +1,10 @@ import NIOConcurrencyHelpers import NIOCore +#if canImport(NIOAsyncRuntime) +import NIOAsyncRuntime +#elseif canImport(NIOPosix) import NIOPosix +#endif import CSQLite import Logging diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift index adf7588..dcdde49 100644 --- a/Sources/SQLiteNIO/SQLiteData.swift +++ b/Sources/SQLiteNIO/SQLiteData.swift @@ -1,13 +1,32 @@ import CSQLite import NIOCore +#if _pointerBitWidth(_64) +// We use Int instead of Int64 on 64 bit systems primarily for +// backwards compatibility, since prior code versions have used Int here. +public typealias SQLiteDataIntegerType = Int // 64-bit platform, Int = 64 bits +#elseif _pointerBitWidth(_32) +public typealias SQLiteDataIntegerType = 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 (eg. _128) when that +// exists. +// +// Or if the following proposal for pointerBitWidth ever lands in a +// published Swift version, then the above conditionals may +// need adjusted: +// +// https://forums.swift.org/t/pitch-pointer-bit-width-compile-time-conditional/59572 +#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(SQLiteDataIntegerType) /// `REAL` affinity, represented in Swift by `Double`. case float(Double) @@ -25,14 +44,14 @@ 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: SQLiteDataIntegerType? { switch self { case .integer(let integer): return integer case .float(let double): - return Int(double) + return SQLiteDataIntegerType(double) case .text(let string): - return Int(string) + return SQLiteDataIntegerType(string) case .blob, .null: return nil } @@ -137,7 +156,7 @@ extension SQLiteData { case SQLITE_NULL: self = .null case SQLITE_INTEGER: - self = .integer(Int(sqlite_nio_sqlite3_value_int64(sqliteValue))) + self = .integer(SQLiteDataIntegerType(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..52f7c3d 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(SQLiteDataIntegerType(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/SQLiteNIOTests.swift b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift index 602ef90..766a453 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 From 4cbc6e9f8774102f0c5232905329a633626f8a95 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 12 Dec 2025 10:55:39 -0700 Subject: [PATCH 2/6] refactor: Use when conditions for thread safety configuration in the manifest, as canImports are deemed to be unsafe to use in manifest files. --- Package.swift | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/Package.swift b/Package.swift index 30edc85..28d94a0 100644 --- a/Package.swift +++ b/Package.swift @@ -66,30 +66,6 @@ var swiftSettings: [SwiftSetting] { [ .enableExperimentalFeature("StrictConcurrency=complete"), ] } -#if canImport(WASILibc) -#if canImport(wasi_pthread) -// Some wasm SDKs provide pthread -// API's, and some future SDKs will as well. -// -// If pthread API's are available, we should use them. -// -// But if not, then we can't use them. -var threadSafetyConfiguration: CSetting { .define("SQLITE_THREADSAFE", to: "1") } -#else -// In this case, we're using a Swift for WebAssembly -// SDK that does NOT have pthread support, so we -// can't compile in thread safety. -// -// Note that by the same principal, the executable will -// almost certainly be single-threaded, so it is acceptable -// and fine to elide thread safety. But in this case, there -// isn't any other option. -var threadSafetyConfiguration: CSetting { .define("SQLITE_THREADSAFE", to: "0") } -#endif -#else -var threadSafetyConfiguration: CSetting { .define("SQLITE_THREADSAFE", to: "1") } -#endif - var sqliteCSettings: [CSetting] { [ // Derived from sqlite3 version 3.43.0 .define("SQLITE_DEFAULT_MEMSTATUS", to: "0"), @@ -121,8 +97,9 @@ var sqliteCSettings: [CSetting] { [ .define("SQLITE_OMIT_TCL_VARIABLE"), .define("SQLITE_OMIT_TRACE"), .define("SQLITE_SECURE_DELETE"), - threadSafetyConfiguration, - .define("SQLITE_THREADSAFE", to: "1"), + .define("SQLITE_THREADSAFE", to: "1", .when(platforms: nonWASIPlatforms)), + // WASI is single threaded and lacks pthread support in official SDKs. + .define("SQLITE_THREADSAFE", to: "0", .when(platforms: wasiPlatform)), .define("SQLITE_UNTESTABLE"), .define("SQLITE_USE_URI"), ] } From c0d0649f0e9579e28e12b6475b3808b4dc712fce Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 12 Dec 2025 10:56:24 -0700 Subject: [PATCH 3/6] refactor: Make the intent more clear and the maintenance more straightforward for the platform conditional setup in the manifest. --- Package.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 28d94a0..0f80a16 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,11 @@ // 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 nonWASIPlatforms: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS, .driverKit, .linux, .windows, .android, .openbsd] let package = Package( name: "sqlite-nio", From 6fbd7bbd0a2b856628f6c6e0e81dd6c8223a8c22 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 12 Dec 2025 11:13:22 -0700 Subject: [PATCH 4/6] refactor: Rename SQLiteDataIntegerType to SQLiteInt64. --- Sources/SQLiteNIO/SQLiteData.swift | 22 +++++++++------------- Sources/SQLiteNIO/SQLiteStatement.swift | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift index dcdde49..99dcb7f 100644 --- a/Sources/SQLiteNIO/SQLiteData.swift +++ b/Sources/SQLiteNIO/SQLiteData.swift @@ -4,9 +4,9 @@ import NIOCore #if _pointerBitWidth(_64) // We use Int instead of Int64 on 64 bit systems primarily for // backwards compatibility, since prior code versions have used Int here. -public typealias SQLiteDataIntegerType = Int // 64-bit platform, Int = 64 bits +public typealias SQLiteInt64 = Int // 64-bit platform, Int = 64 bits #elseif _pointerBitWidth(_32) -public typealias SQLiteDataIntegerType = Int64 // On 32-bit platforms, we want to use 64 bit integers. +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 (eg. _128) when that @@ -26,7 +26,7 @@ public typealias SQLiteDataIntegerType = Int64 // On 32-bit platforms, we want t /// innate affinity. public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable { /// `INTEGER` affinity, represented in Swift by `Int`. - case integer(SQLiteDataIntegerType) + case integer(SQLiteInt64) /// `REAL` affinity, represented in Swift by `Double`. case float(Double) @@ -44,16 +44,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: SQLiteDataIntegerType? { + public var integer: SQLiteInt64? { switch self { - case .integer(let integer): - return integer - case .float(let double): - return SQLiteDataIntegerType(double) - case .text(let string): - return SQLiteDataIntegerType(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 } } @@ -156,7 +152,7 @@ extension SQLiteData { case SQLITE_NULL: self = .null case SQLITE_INTEGER: - self = .integer(SQLiteDataIntegerType(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/SQLiteStatement.swift b/Sources/SQLiteNIO/SQLiteStatement.swift index 52f7c3d..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(SQLiteDataIntegerType(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: From 37416249a3a8cb26e42a7569ca0f1c39a411dbad Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 12 Dec 2025 11:13:52 -0700 Subject: [PATCH 5/6] docs: Update some code comments on integer bit width --- Sources/SQLiteNIO/SQLiteData.swift | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift index 99dcb7f..8455a15 100644 --- a/Sources/SQLiteNIO/SQLiteData.swift +++ b/Sources/SQLiteNIO/SQLiteData.swift @@ -2,21 +2,14 @@ import CSQLite import NIOCore #if _pointerBitWidth(_64) -// We use Int instead of Int64 on 64 bit systems primarily for -// backwards compatibility, since prior code versions have used Int here. +/// 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 (eg. _128) when that -// exists. -// -// Or if the following proposal for pointerBitWidth ever lands in a -// published Swift version, then the above conditionals may -// need adjusted: -// -// https://forums.swift.org/t/pitch-pointer-bit-width-compile-time-conditional/59572 +/// 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 From e90448c3bc639485f5f57f4b1fe414bc6b8ca7d3 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 15 Dec 2025 17:57:45 -0700 Subject: [PATCH 6/6] build: Require at least the 1.0.0 version of nio-async-runtime --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 0f80a16..81dc20d 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.89.0"), - .package(url: "https://github.com/PassiveLogic/nio-async-runtime.git", from: "0.0.2"), + .package(url: "https://github.com/PassiveLogic/nio-async-runtime.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), ], targets: [