From f00a97d4c9e25d1b6434139160d3dbdeda1fd1fd Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Tue, 28 Jul 2026 21:09:48 -0600 Subject: [PATCH] feat: compile SQLiteNIO under Embedded Swift Motivation: Embedded Swift has no reflection, no `Encoder`, no existential metatypes, no generic protocol requirements, and no Foundation in either flavor. A handful of SQLiteNIO declarations depend on one of those and are the only thing standing between the SwiftNIO-free configuration and a `wasm32-unknown-wasip1-embedded` build. Modifications: Elide exactly those declarations. Every gate is `#if hasFeature(Embedded)` or a Foundation-flavor `canImport`, so no other target is affected in any way. - `SQLiteData`'s `Encodable` conformance and the deprecated `SQLiteDataType.serialize(_:)` move behind `#if !hasFeature(Embedded)`: Embedded Swift has no `Encoder` and no `any Encodable` existential. - `SQLiteDatabase`'s `async` `withConnection(_:)` requirement moves behind the same gate: a generic method requirement cannot be placed in a witness table, and keeping it would make `any SQLiteDatabase` unusable under Embedded Swift. The concrete ``SQLiteConnection/withConnection(_:)`` remains available. - `SQLiteError: LocalizedError`, the `Data` bridges, and the `Date` bridge gate on `canImport(FoundationEssentials) || canImport(Foundation)`. The `errorDescription` witness stays in the type body, so the conformance is the only conditional part. - The hook API gates whole-file on the same condition: hook events carry `Date` timestamps and observer identity rides on `UUID`, so the API cannot exist without one of the flavors. The `observerBuckets` container and `close()`'s `clearAllHooks()` call gate with it. - `Exports.swift`'s `ByteBuffer(data:)` shim, and its Foundation-flavor import, move behind the same gate; the rest of the `[UInt8]` stand-in needs only the stdlib. - `SQLiteRow.description` and `SQLiteCustomFunction`'s error reporting use reflection (`Array.description`, `any Error` interpolation); reflection-free equivalents replace them under Embedded Swift. Result: SQLiteNIO compiles for `wasm32-unknown-wasip1-embedded`. Building for that target additionally requires an Embedded-clean `apple/swift-log`; see the pull request description. --- Sources/SQLiteNIO/Exports.swift | 25 +++++++++++-------- .../SQLiteNIO/SQLiteConnection+Hooks.swift | 7 ++++++ Sources/SQLiteNIO/SQLiteConnection.swift | 6 +++++ Sources/SQLiteNIO/SQLiteCustomFunction.swift | 5 ++++ Sources/SQLiteNIO/SQLiteData.swift | 13 ++++++++-- Sources/SQLiteNIO/SQLiteDataConvertible.swift | 8 +++++- Sources/SQLiteNIO/SQLiteDataType.swift | 4 +++ Sources/SQLiteNIO/SQLiteDatabase.swift | 7 ++++++ Sources/SQLiteNIO/SQLiteError.swift | 10 ++++++-- Sources/SQLiteNIO/SQLiteRow.swift | 5 ++++ 10 files changed, 75 insertions(+), 15 deletions(-) diff --git a/Sources/SQLiteNIO/Exports.swift b/Sources/SQLiteNIO/Exports.swift index e3a72d0..fc2198c 100644 --- a/Sources/SQLiteNIO/Exports.swift +++ b/Sources/SQLiteNIO/Exports.swift @@ -5,12 +5,6 @@ @_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoopGroup @_documentation(visibility: internal) @_exported import class NIOPosix.MultiThreadedEventLoopGroup #else // !canImport(NIOCore) -#if canImport(FoundationEssentials) -import FoundationEssentials -#else -import Foundation -#endif - /// `BLOB` values are carried by `[UInt8]` on platforms where SwiftNIO is not available. /// /// SwiftNIO does not support `wasm32-unknown-wasip1`, so `NIOCore.ByteBuffer` cannot be re-exported @@ -29,10 +23,6 @@ extension ByteBuffer { self.init(bytes) } - init(data: Data) { - self.init(data) - } - var readableBytes: Int { self.count } @@ -46,6 +36,21 @@ extension ByteBuffer { } } +// The `Data` bridge needs a Foundation flavor, and Embedded Swift has neither. +#if canImport(FoundationEssentials) || canImport(Foundation) +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +extension ByteBuffer { + init(data: Data) { + self.init(data) + } +} +#endif // canImport(FoundationEssentials) || canImport(Foundation) + /// A single-threaded stand-in for `NIOConcurrencyHelpers.NIOLockedValueBox`. /// /// The one supported SwiftNIO-free target is single-threaded, so no lock is needed and none is diff --git a/Sources/SQLiteNIO/SQLiteConnection+Hooks.swift b/Sources/SQLiteNIO/SQLiteConnection+Hooks.swift index be96916..395509c 100644 --- a/Sources/SQLiteNIO/SQLiteConnection+Hooks.swift +++ b/Sources/SQLiteNIO/SQLiteConnection+Hooks.swift @@ -1,3 +1,9 @@ +// N.B.: This is elided wholesale, rather than narrowed, because the hook API's *public* surface is +// Foundation-shaped: `SQLiteCommitEvent.date` and `SQLiteRollbackEvent.date` are `Date`, and +// `SQLiteHookToken.id` is a `UUID`. Embedded Swift has neither type in either Foundation flavor, +// so keeping the API here would mean either shipping stand-ins for both or giving Embedded a +// different public API than every other target. Neither belongs in this commit. +#if canImport(FoundationEssentials) || canImport(Foundation) #if canImport(FoundationEssentials) import FoundationEssentials #else @@ -871,3 +877,4 @@ extension SQLiteConnection { self.observerBuckets.withLockedValue { $0 = .init() } } } +#endif // canImport(FoundationEssentials) || canImport(Foundation) diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift index 20f67e2..a6bbfbc 100644 --- a/Sources/SQLiteNIO/SQLiteConnection.swift +++ b/Sources/SQLiteNIO/SQLiteConnection.swift @@ -253,8 +253,11 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable { /// The underlying `sqlite3` connection handle. let handle: SQLiteConnectionHandle + // The hook API needs a Foundation flavor; see `SQLiteConnection+Hooks.swift`. + #if canImport(FoundationEssentials) || canImport(Foundation) /// Container for storing multiple observers per hook type. let observerBuckets = NIOLockedValueBox(.init()) + #endif #if canImport(NIOCore) /// The thread pool used by this connection when calling libsqlite3 APIs. @@ -499,7 +502,10 @@ extension SQLiteConnection { /// No further operations may be performed on the connection after calling this method. public func close() async throws { try await self.withBlockingIO { + // The hook API needs a Foundation flavor; see `SQLiteConnection+Hooks.swift`. + #if canImport(FoundationEssentials) || canImport(Foundation) self.clearAllHooks() + #endif sqlite_nio_sqlite3_close(self.handle.raw) self.handle.raw = nil } diff --git a/Sources/SQLiteNIO/SQLiteCustomFunction.swift b/Sources/SQLiteNIO/SQLiteCustomFunction.swift index 8698783..b3cf95c 100644 --- a/Sources/SQLiteNIO/SQLiteCustomFunction.swift +++ b/Sources/SQLiteNIO/SQLiteCustomFunction.swift @@ -311,7 +311,12 @@ public final class SQLiteCustomFunction: Hashable { sqlite_nio_sqlite3_result_error(sqliteContext, error.message, -1) sqlite_nio_sqlite3_result_error_code(sqliteContext, error.reason.statusCode) } else { + #if hasFeature(Embedded) + // Interpolating an `any Error` needs reflection, unavailable in Embedded Swift. + sqlite_nio_sqlite3_result_error(sqliteContext, "custom function error", -1) + #else sqlite_nio_sqlite3_result_error(sqliteContext, "\(error)", -1) + #endif } } } diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift index 860a57e..0b03d06 100644 --- a/Sources/SQLiteNIO/SQLiteData.swift +++ b/Sources/SQLiteNIO/SQLiteData.swift @@ -16,7 +16,10 @@ public typealias SQLiteInt64 = Int /// /// 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 { +/// +/// > Note: The `Encodable` conformance is declared in a conditional extension below, because +/// > `Encoder` is unavailable in Embedded Swift. +public enum SQLiteData: Equatable, CustomStringConvertible, Sendable { /// `INTEGER` affinity, represented in Swift by `Int`. case integer(SQLiteInt64) @@ -112,7 +115,8 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable } } - // See `Encodable.encode(to:)`. + // See `Encodable.encode(to:)`. `Encoder` is unavailable in Embedded Swift. + #if !hasFeature(Embedded) public func encode(to encoder: any Encoder) throws { var container = encoder.singleValueContainer() switch self { @@ -123,8 +127,13 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible, Sendable case .null: try container.encodeNil() } } + #endif // !hasFeature(Embedded) } +#if !hasFeature(Embedded) +extension SQLiteData: Encodable {} +#endif + extension SQLiteData { /// Attempt to interpret an `sqlite3_value` as an equivalent ``SQLiteData``. init(sqliteValue: OpaquePointer) throws { diff --git a/Sources/SQLiteNIO/SQLiteDataConvertible.swift b/Sources/SQLiteNIO/SQLiteDataConvertible.swift index 67deaa2..9aa8d16 100644 --- a/Sources/SQLiteNIO/SQLiteDataConvertible.swift +++ b/Sources/SQLiteNIO/SQLiteDataConvertible.swift @@ -20,7 +20,7 @@ import NIOCore import NIOFoundationEssentialsCompat #endif import FoundationEssentials -#else +#elseif canImport(Foundation) #if canImport(NIOCore) import NIOFoundationCompat #endif @@ -112,6 +112,8 @@ extension ByteBuffer: SQLiteDataConvertible { } } +// The `Data` bridge needs a Foundation flavor, and Embedded Swift has neither. +#if canImport(FoundationEssentials) || canImport(Foundation) extension Data: SQLiteDataConvertible { public init?(sqliteData: SQLiteData) { guard case .blob(let value) = sqliteData else { @@ -126,6 +128,7 @@ extension Data: SQLiteDataConvertible { .blob(.init(data: self)) } } +#endif // canImport(FoundationEssentials) || canImport(Foundation) extension Bool: SQLiteDataConvertible { public init?(sqliteData: SQLiteData) { @@ -140,6 +143,8 @@ extension Bool: SQLiteDataConvertible { } } +// The `Date` bridge needs a Foundation flavor, and Embedded Swift has neither. +#if canImport(FoundationEssentials) || canImport(Foundation) extension Date: SQLiteDataConvertible { public init?(sqliteData: SQLiteData) { let value: Double @@ -203,3 +208,4 @@ extension Date: SQLiteDataConvertible { .float(self.timeIntervalSince1970) } } +#endif // canImport(FoundationEssentials) || canImport(Foundation) diff --git a/Sources/SQLiteNIO/SQLiteDataType.swift b/Sources/SQLiteNIO/SQLiteDataType.swift index a899699..8e4dd0d 100644 --- a/Sources/SQLiteNIO/SQLiteDataType.swift +++ b/Sources/SQLiteNIO/SQLiteDataType.swift @@ -16,6 +16,9 @@ public enum SQLiteDataType { /// `NULL`. case null + // `any Encodable` requires runtime existentials that Embedded Swift does not provide. The + // method is deprecated and unused, so it is simply elided there. + #if !hasFeature(Embedded) public func serialize(_ binds: inout [any Encodable]) -> String { switch self { case .integer: return "INTEGER" @@ -25,4 +28,5 @@ public enum SQLiteDataType { case .null: return "NULL" } } + #endif // !hasFeature(Embedded) } diff --git a/Sources/SQLiteNIO/SQLiteDatabase.swift b/Sources/SQLiteNIO/SQLiteDatabase.swift index bc147c7..695ac9b 100644 --- a/Sources/SQLiteNIO/SQLiteDatabase.swift +++ b/Sources/SQLiteNIO/SQLiteDatabase.swift @@ -79,6 +79,10 @@ public protocol SQLiteDatabase: Sendable { ) -> EventLoopFuture #endif // canImport(NIOCore) + // Unavailable in Embedded Swift: a generic method requirement cannot be placed in a witness + // table, so keeping it would make `any SQLiteDatabase` unusable there. + // ``SQLiteConnection/withConnection(_:)`` remains available on the concrete type. + #if !hasFeature(Embedded) /// Call the provided closure with a concrete ``SQLiteConnection`` instance, concurrency version. /// /// This method is required to provide a connection object which executes all queries directed to it in the @@ -92,6 +96,7 @@ public protocol SQLiteDatabase: Sendable { func withConnection( _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T ) async throws -> T + #endif // !hasFeature(Embedded) } /// Convenience helpers and Concurrency-aware variants. @@ -202,10 +207,12 @@ private struct SQLiteDatabaseCustomLogger: SQLiteDatabase { self.database.withConnection(closure) } #endif // canImport(NIOCore) + #if !hasFeature(Embedded) // See `SQLiteDatabase.withConnection(_:)`. func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T) async throws -> T { try await self.database.withConnection(closure) } + #endif #if canImport(NIOCore) // See `SQLiteDatabase.query(_:_:_:)`. diff --git a/Sources/SQLiteNIO/SQLiteError.swift b/Sources/SQLiteNIO/SQLiteError.swift index be0316d..7d5e29a 100644 --- a/Sources/SQLiteNIO/SQLiteError.swift +++ b/Sources/SQLiteNIO/SQLiteError.swift @@ -1,11 +1,17 @@ import VaporCSQLite #if canImport(FoundationEssentials) import FoundationEssentials -#else +#elseif canImport(Foundation) import Foundation #endif -public struct SQLiteError: Error, CustomStringConvertible, LocalizedError { +// `LocalizedError` needs one of the Foundation flavors, and Embedded Swift has neither. The +// `errorDescription` witness stays in the type body; only the conformance is conditional. +#if canImport(FoundationEssentials) || canImport(Foundation) +extension SQLiteError: LocalizedError {} +#endif + +public struct SQLiteError: Error, CustomStringConvertible { public let reason: Reason public let message: String diff --git a/Sources/SQLiteNIO/SQLiteRow.swift b/Sources/SQLiteNIO/SQLiteRow.swift index abddcad..b2c49fd 100644 --- a/Sources/SQLiteNIO/SQLiteRow.swift +++ b/Sources/SQLiteNIO/SQLiteRow.swift @@ -25,7 +25,12 @@ public struct SQLiteRow: CustomStringConvertible, Sendable { } public var description: String { + #if hasFeature(Embedded) + // `Array.description` goes through reflection, which Embedded Swift does not provide. + "[" + self.columns.map { $0.description }.joined(separator: ", ") + "]" + #else self.columns.description + #endif } }