Skip to content
Open
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
ios_scheme_name: sqlite-nio
with_musl: true
with_android: true
with_wasm: true

submit-dependencies:
permissions:
Expand Down
17 changes: 14 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// swift-tools-version:6.1
import PackageDescription

/// `.when(platforms:)` can only include, never exclude, so excluding WASI means listing everything else.
/// This list matches the [supported platforms on the Swift 6.1 release of SPM](https://github.com/swiftlang/swift-package-manager/blob/release/6.1/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-nio",
platforms: [
Expand Down Expand Up @@ -37,11 +43,16 @@ let package = Package(
dependencies: [
.target(name: "VaporCSQLite"),
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
// SwiftNIO does not support wasm32-unknown-wasip1: NIOPosix is built around POSIX
// sockets and threads, neither of which WASI preview 1 provides. On WASI these
// products are therefore not linked, and the `#if canImport(NIOCore)` gates in
// Sources/ drop the `EventLoopFuture` API in favor of the `async` one.
// (`NIOFoundationCompat` keeps its Darwin-only condition, which already excludes WASI.)
.product(name: "NIOCore", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)),
.product(name: "NIOPosix", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)),
.product(name: "NIOFoundationCompat", package: "swift-nio",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .watchOS, .macCatalyst, .visionOS])),
.product(name: "NIOFoundationEssentialsCompat", package: "swift-nio"),
.product(name: "NIOFoundationEssentialsCompat", package: "swift-nio", condition: .when(platforms: nonWASIPlatforms)),
],
swiftSettings: swiftSettings
),
Expand Down
75 changes: 75 additions & 0 deletions Sources/SQLiteNIO/Exports.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
#if canImport(NIOCore)
@_documentation(visibility: internal) @_exported import struct NIOCore.ByteBuffer
@_documentation(visibility: internal) @_exported import class NIOPosix.NIOThreadPool
@_documentation(visibility: internal) @_exported import protocol NIOCore.EventLoop
@_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
/// above (see the product conditions in `Package.swift`). Declaring the substitution as a typealias
/// rather than forking every declaration that mentions `ByteBuffer` keeps ``SQLiteData`` and friends
/// as a single implementation across both configurations.
public typealias ByteBuffer = [UInt8]

/// The subset of `NIOCore.ByteBuffer`'s API that this package uses, expressed over `[UInt8]`.
///
/// These are deliberately not `public`: they exist only so the shared implementation compiles
/// unchanged, and making them public would graft NIO-flavored members onto every `[UInt8]` in any
/// module that imports `SQLiteNIO`.
extension ByteBuffer {
init(bytes: some Sequence<UInt8>) {
self.init(bytes)
}

init(data: Data) {
self.init(data)
}

var readableBytes: Int {
self.count
}

var readableBytesView: Self {
self
}

func withUnsafeReadableBytes<T>(_ body: (UnsafeRawBufferPointer) throws -> T) rethrows -> T {
try self.withUnsafeBytes(body)
}
}

// N.B.: A tripwire, not a platform gate. The stand-in below takes no lock, so it only supports
// single-threaded use: `wasm32-unknown-wasip1` qualifies, `wasm32-unknown-wasip1-threads` does not.
#if _runtime(_multithreaded)
#error("""
SQLiteNIO's `canImport(NIOCore)` fallback has been selected for a multithreaded runtime, and the \
`NIOLockedValueBox` below does not support multithreaded locking. Please use a \
multithreading-capable lock for this platform.
""")
#endif

/// 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
/// taken: the box exists so the `sqlite3_initialize()` guard and the hook observer storage in
/// ``SQLiteConnection`` read the same in both configurations. `@unchecked Sendable` is sound for
/// the same reason.
final class NIOLockedValueBox<Value>: @unchecked Sendable {
private var value: Value
private var isLocked = false

init(_ value: Value) {
self.value = value
}

func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
assert(!self.isLocked, "NIOLockedValueBox was re-entered unexpectedly; this implementation supports single-threaded use only")
self.isLocked = true
defer { self.isLocked = false }
return try mutate(&self.value)
}
}
#endif // !canImport(NIOCore)
14 changes: 8 additions & 6 deletions Sources/SQLiteNIO/SQLiteConnection+Hooks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import FoundationEssentials
#else
import Foundation
#endif
#if canImport(NIOCore)
import NIOConcurrencyHelpers
import NIOCore
#endif // canImport(NIOCore)
import VaporCSQLite

// MARK: - Hook Types and Events
Expand Down Expand Up @@ -495,7 +497,7 @@ extension SQLiteConnection {
/// - Parameter callback: Closure to invoke when update events occur.
/// - Returns: A ``SQLiteHookToken`` that removes the observer when canceled.
public func addUpdateObserver(lifetime: SQLiteObserverLifetime, _ callback: @escaping SQLiteUpdateHookCallback) async throws -> SQLiteHookToken {
try await self.threadPool.runIfActive {
try await self.withBlockingIO {
let id = UUID()

self.addHookAndInstallDispatcherIfNeeded(kind: .update, action: { $0.updateHooks[id] = callback })
Expand Down Expand Up @@ -526,7 +528,7 @@ extension SQLiteConnection {
/// - Parameter callback: Closure to invoke when commit events occur.
/// - Returns: A ``SQLiteHookToken`` that removes the observer when canceled.
public func addCommitObserver(lifetime: SQLiteObserverLifetime, _ callback: @escaping SQLiteCommitObserver) async throws -> SQLiteHookToken {
try await self.threadPool.runIfActive {
try await self.withBlockingIO {
let id = UUID()

self.addHookAndInstallDispatcherIfNeeded(kind: .commit, action: { $0.commitObservers[id] = callback })
Expand Down Expand Up @@ -557,7 +559,7 @@ extension SQLiteConnection {
/// - Parameter callback: Closure to invoke when commit events occur.
/// - Returns: A ``SQLiteHookToken`` that removes the validator when canceled.
public func setCommitValidator(lifetime: SQLiteObserverLifetime, _ callback: @escaping SQLiteCommitValidator) async throws -> SQLiteHookToken {
try await self.threadPool.runIfActive {
try await self.withBlockingIO {
self.addHookAndInstallDispatcherIfNeeded(kind: .commit, action: { $0.commitValidator = callback })
return .init(lifetime: lifetime) { @Sendable [weak self = self] in
_ = self?.removeHookAndUninstallDispatcherIfNeeded(kind: .commit, action: { $0.commitValidator = nil })
Expand Down Expand Up @@ -585,7 +587,7 @@ extension SQLiteConnection {
/// - Parameter callback: Closure to invoke when rollback events occur.
/// - Returns: A ``SQLiteHookToken`` that removes the observer when canceled.
public func addRollbackObserver(lifetime: SQLiteObserverLifetime, _ callback: @escaping SQLiteRollbackHookCallback) async throws -> SQLiteHookToken {
try await self.threadPool.runIfActive {
try await self.withBlockingIO {
let id = UUID()

self.addHookAndInstallDispatcherIfNeeded(kind: .rollback, action: { $0.rollbackHooks[id] = callback })
Expand Down Expand Up @@ -616,7 +618,7 @@ extension SQLiteConnection {
/// - Parameter callback: Closure to invoke when authorization events occur.
/// - Returns: A ``SQLiteHookToken`` that removes the observer when canceled.
public func addAuthorizerObserver(lifetime: SQLiteObserverLifetime, _ callback: @escaping SQLiteAuthorizerObserver) async throws -> SQLiteHookToken {
try await self.threadPool.runIfActive {
try await self.withBlockingIO {
let id = UUID()

self.addHookAndInstallDispatcherIfNeeded(kind: .authorizer, action: { $0.authorizerObservers[id] = callback })
Expand Down Expand Up @@ -650,7 +652,7 @@ extension SQLiteConnection {
/// - Parameter callback: Closure to invoke when authorization events occur.
/// - Returns: A ``SQLiteHookToken`` that removes the validator when canceled.
public func setAuthorizerValidator(lifetime: SQLiteObserverLifetime, _ callback: @escaping SQLiteAuthorizerValidator) async throws -> SQLiteHookToken {
try await self.threadPool.runIfActive {
try await self.withBlockingIO {
self.addHookAndInstallDispatcherIfNeeded(kind: .authorizer, action: { $0.authorizerValidator = callback })
return .init(lifetime: lifetime) { @Sendable [weak self = self] in
_ = self?.removeHookAndUninstallDispatcherIfNeeded(kind: .commit, action: { $0.authorizerValidator = nil })
Expand Down
Loading
Loading