forked from vapor/sqlite-nio
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: NativeConcurrency package trait — NIO-free Swift-concurrency backend for SQLiteNIO #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
scottmarchant
wants to merge
10
commits into
feat/khasmPAL-2026
Choose a base branch
from
feat/native-concurrency-trait
base: feat/khasmPAL-2026
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
c9f6783
build(embedded): use local NIO/log clones and gate NIOPosix off WASI
scottmarchant bfb68ca
feat(embedded): add SwiftNIO/EmbeddedWASI SPM traits (Package@swift-6.1)
scottmarchant 5e40d7d
build(embedded): elide swift-nio on WASI via platform condition (drop…
scottmarchant 127e740
feat(embedded): NIO-free SQLiteData (ByteBuffer->[UInt8], gated Encod…
scottmarchant 8f8a63a
feat(embedded): NIO-free SQLiteStatement blob handling + gate NIO re-…
scottmarchant df39008
feat(embedded): NIO-free async SQLiteConnection + SQLiteDatabase for …
scottmarchant b921581
feat(embedded): drop Foundation/ByteBuffer from SQLiteNIO value + err…
scottmarchant 8fd719a
refactor(embedded): make the NIO-free path Embedded-only (flavor-safe…
scottmarchant 7f41021
build(embedded): reference swift-nio via the PassiveLogic fork URL, n…
scottmarchant 3591a81
feat: NativeConcurrency package trait — NIO-free Swift-concurrency ba…
scottmarchant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // swift-tools-version:6.1 | ||
| 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: [ | ||
| .macOS(.v10_15), | ||
| .iOS(.v13), | ||
| .watchOS(.v6), | ||
| .tvOS(.v13), | ||
| ], | ||
| products: [ | ||
| .library(name: "SQLiteNIO", targets: ["SQLiteNIO"]), | ||
| ], | ||
| traits: [ | ||
| .default(enabledTraits: ["NIO"]), | ||
| .trait( | ||
| name: "NIO", | ||
| description: "Default backend: SwiftNIO (EventLoopFuture/ByteBuffer, NIOThreadPool)." | ||
| ), | ||
| .trait( | ||
| name: "NativeConcurrency", | ||
| description: "NIO-free backend on Swift concurrency (async/await, [UInt8] blobs). Build with `--traits NativeConcurrency` (replaces the default NIO backend)." | ||
| ), | ||
| .trait( | ||
| name: "Freestanding", | ||
| description: "Embedded/freestanding flavor (implies NativeConcurrency). No additional source effect in this package beyond NativeConcurrency; declared so a root's `--traits Freestanding` configuration names a known trait when this package is wired by path.", | ||
| enabledTraits: ["NativeConcurrency"] | ||
| ), | ||
| ], | ||
| dependencies: [ | ||
| // 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/khasmPAL-2026"), | ||
| .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), | ||
| ], | ||
| targets: [ | ||
| .plugin( | ||
| name: "VendorSQLite", | ||
| capability: .command( | ||
| intent: .custom(verb: "vendor-sqlite", description: "Vendor SQLite"), | ||
| permissions: [ | ||
| .allowNetworkConnections(scope: .all(ports: [443]), reason: "Retrieve the latest build of SQLite"), | ||
| .writeToPackageDirectory(reason: "Update the vendored SQLite files"), | ||
| ] | ||
| ), | ||
| exclude: ["001-warnings-and-data-race.patch"] | ||
| ), | ||
| .target( | ||
| name: "CSQLite", | ||
| cSettings: sqliteCSettings | ||
| ), | ||
| .target( | ||
| name: "SQLiteNIO", | ||
| dependencies: [ | ||
| .target(name: "CSQLite"), | ||
| .product(name: "Logging", package: "swift-log"), | ||
| // The SwiftNIO stack rides the default `NIO` trait. With `NativeConcurrency` | ||
| // enabled instead, SQLiteNIO is a NIO-free, Swift-Concurrency driver over | ||
| // CSQLite, gated in source with `#if NativeConcurrency`. | ||
| .product(name: "NIOCore", package: "swift-nio", condition: .when(traits: ["NIO"])), | ||
| .product(name: "NIOAsyncRuntime", package: "swift-nio", condition: .when(platforms: wasiPlatform, traits: ["NIO"])), | ||
| .product(name: "NIOPosix", package: "swift-nio", condition: .when(traits: ["NIO"])), | ||
| .product(name: "NIOFoundationCompat", package: "swift-nio", condition: .when(traits: ["NIO"])), | ||
| ], | ||
| swiftSettings: swiftSettings | ||
| ), | ||
| .testTarget( | ||
| name: "SQLiteNIOTests", | ||
| dependencies: [ | ||
| .target(name: "SQLiteNIO"), | ||
| ], | ||
| swiftSettings: swiftSettings | ||
| ), | ||
| ], | ||
| swiftLanguageModes: [.v5] | ||
| ) | ||
|
|
||
| var swiftSettings: [SwiftSetting] { [ | ||
| // This manifest raises the tools-version to 6.1 (for package traits); the package sources | ||
| // stay in the Swift 5 language mode of the base manifest. | ||
| .swiftLanguageMode(.v5), | ||
| .enableUpcomingFeature("ExistentialAny"), | ||
| .enableUpcomingFeature("ConciseMagicFile"), | ||
| .enableUpcomingFeature("ForwardTrailingClosures"), | ||
| .enableUpcomingFeature("DisableOutwardActorInference"), | ||
| .enableExperimentalFeature("StrictConcurrency=complete"), | ||
| ] } | ||
|
|
||
| var sqliteCSettings: [CSetting] { [ | ||
| // Derived from sqlite3 version 3.43.0 | ||
| .define("SQLITE_DEFAULT_MEMSTATUS", to: "0"), | ||
| .define("SQLITE_DISABLE_PAGECACHE_OVERFLOW_STATS"), | ||
| .define("SQLITE_DQS", to: "0"), | ||
| .define("SQLITE_ENABLE_API_ARMOR", .when(configuration: .debug)), | ||
| .define("SQLITE_ENABLE_COLUMN_METADATA"), | ||
| .define("SQLITE_ENABLE_DBSTAT_VTAB"), | ||
| .define("SQLITE_ENABLE_FTS3"), | ||
| .define("SQLITE_ENABLE_FTS3_PARENTHESIS"), | ||
| .define("SQLITE_ENABLE_FTS3_TOKENIZER"), | ||
| .define("SQLITE_ENABLE_FTS4"), | ||
| .define("SQLITE_ENABLE_FTS5"), | ||
| .define("SQLITE_ENABLE_NULL_TRIM"), | ||
| .define("SQLITE_ENABLE_RTREE"), | ||
| .define("SQLITE_ENABLE_SESSION"), | ||
| .define("SQLITE_ENABLE_STMTVTAB"), | ||
| .define("SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION"), | ||
| .define("SQLITE_ENABLE_UNLOCK_NOTIFY"), | ||
| .define("SQLITE_MAX_VARIABLE_NUMBER", to: "250000"), | ||
| .define("SQLITE_LIKE_DOESNT_MATCH_BLOBS"), | ||
| .define("SQLITE_OMIT_COMPLETE"), | ||
| .define("SQLITE_OMIT_DEPRECATED"), | ||
| .define("SQLITE_OMIT_DESERIALIZE"), | ||
| .define("SQLITE_OMIT_GET_TABLE"), | ||
| .define("SQLITE_OMIT_LOAD_EXTENSION"), | ||
| .define("SQLITE_OMIT_PROGRESS_CALLBACK"), | ||
| .define("SQLITE_OMIT_SHARED_CACHE"), | ||
| .define("SQLITE_OMIT_TCL_VARIABLE"), | ||
| .define("SQLITE_OMIT_TRACE"), | ||
| .define("SQLITE_SECURE_DELETE"), | ||
| .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"), | ||
| ] } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,9 @@ struct VendorSQLite: CommandPlugin { | |
| static let sqliteURL = URL(string: "https://sqlite.org")! | ||
| static let vendorPrefix = "sqlite_nio" | ||
|
|
||
| static var verbose = false | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to check the branch targets here. I thought this was supposed to be based on the latest main of sqlite-nio |
||
| // `nonisolated(unsafe)`: set once from `performCommand()` before any concurrent work; needed | ||
| // because plugins compile in the Swift 6 language mode under the tools-6.1 traits manifest. | ||
| nonisolated(unsafe) static var verbose = false | ||
|
|
||
| var verbose: Bool { Self.verbose } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| #if NativeConcurrency | ||
| #if canImport(Darwin) | ||
| import Darwin | ||
| #elseif canImport(Glibc) | ||
| import Glibc | ||
| #elseif canImport(Musl) | ||
| import Musl | ||
| #elseif canImport(Android) | ||
| import Android | ||
| #endif | ||
|
|
||
| /// A minimal mutex-protected value box for the NativeConcurrency (NIO-free) build. | ||
| /// | ||
| /// `NIOConcurrencyHelpers.NIOLock` is unavailable here (NIO is not linked), and | ||
| /// `Synchronization.Mutex` would force a platform-floor bump (macOS 15 et al.), so this uses | ||
| /// `os_unfair_lock` on Darwin and `pthread_mutex_t` elsewhere. On single-threaded targets with | ||
| /// no lock primitive (Embedded WASI) it degrades to direct access, which is sound because that | ||
| /// runtime has exactly one thread. | ||
| final class NativeConcurrencyLockedBox<Value>: @unchecked Sendable { | ||
| #if hasFeature(Embedded) || os(WASI) | ||
| private var value: Value | ||
|
|
||
| init(_ value: Value) { | ||
| self.value = value | ||
| } | ||
|
|
||
| func withLock<Result>(_ body: (inout Value) throws -> Result) rethrows -> Result { | ||
| try body(&self.value) | ||
| } | ||
| #elseif canImport(Darwin) | ||
| private let lockPointer: os_unfair_lock_t | ||
| private var value: Value | ||
|
|
||
| init(_ value: Value) { | ||
| self.lockPointer = .allocate(capacity: 1) | ||
| self.lockPointer.initialize(to: os_unfair_lock()) | ||
| self.value = value | ||
| } | ||
|
|
||
| deinit { | ||
| self.lockPointer.deinitialize(count: 1) | ||
| self.lockPointer.deallocate() | ||
| } | ||
|
|
||
| func withLock<Result>(_ body: (inout Value) throws -> Result) rethrows -> Result { | ||
| os_unfair_lock_lock(self.lockPointer) | ||
| defer { os_unfair_lock_unlock(self.lockPointer) } | ||
| return try body(&self.value) | ||
| } | ||
| #else | ||
| private let mutexPointer: UnsafeMutablePointer<pthread_mutex_t> | ||
| private var value: Value | ||
|
|
||
| init(_ value: Value) { | ||
| self.mutexPointer = .allocate(capacity: 1) | ||
| self.mutexPointer.initialize(to: pthread_mutex_t()) | ||
| pthread_mutex_init(self.mutexPointer, nil) | ||
| self.value = value | ||
| } | ||
|
|
||
| deinit { | ||
| pthread_mutex_destroy(self.mutexPointer) | ||
| self.mutexPointer.deinitialize(count: 1) | ||
| self.mutexPointer.deallocate() | ||
| } | ||
|
|
||
| func withLock<Result>(_ body: (inout Value) throws -> Result) rethrows -> Result { | ||
| pthread_mutex_lock(self.mutexPointer) | ||
| defer { pthread_mutex_unlock(self.mutexPointer) } | ||
| return try body(&self.value) | ||
| } | ||
| #endif | ||
| } | ||
| #endif // NativeConcurrency |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
154 changes: 154 additions & 0 deletions
154
Sources/SQLiteNIO/SQLiteConnection+NativeConcurrency.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| #if NativeConcurrency | ||
| import CSQLite | ||
| import Logging | ||
|
|
||
| /// A wrapper for the `OpaquePointer` used to represent an open `sqlite3` handle. | ||
| /// | ||
| /// The pointer itself is guarded by a lock (it is read on every query and written to `nil` on | ||
| /// close); the SQLite handle behind it is opened `SQLITE_OPEN_FULLMUTEX`, so concurrent use of | ||
| /// the handle is serialized by SQLite itself, as on the SwiftNIO build. On single-threaded | ||
| /// targets (Embedded WASI) the lock degrades to direct access. | ||
| final class SQLiteConnectionHandle: @unchecked Sendable { | ||
| private let storage: NativeConcurrencyLockedBox<OpaquePointer?> | ||
|
|
||
| var raw: OpaquePointer? { | ||
| get { self.storage.withLock { $0 } } | ||
| set { self.storage.withLock { $0 = newValue } } | ||
| } | ||
|
|
||
| init(_ raw: OpaquePointer?) { | ||
| self.storage = .init(raw) | ||
| } | ||
| } | ||
|
|
||
| /// A single open connection to an SQLite database (NativeConcurrency build). | ||
| /// | ||
| /// This is the NIO-free variant: it exposes a Swift-Concurrency (`async`/`await`) API over CSQLite | ||
| /// with no `EventLoopFuture`, `EventLoopGroup`, or `NIOThreadPool`. The blocking libsqlite3 calls | ||
| /// run inline on the calling task. The observable hook API and connection pooling are not | ||
| /// available on this build. | ||
| public final class SQLiteConnection: SQLiteDatabase, Sendable { | ||
| /// The possible storage types for an SQLite database. | ||
| public enum Storage: Equatable, Sendable { | ||
| /// An SQLite database stored entirely in memory. | ||
| case memory | ||
|
|
||
| /// An SQLite database stored in a file at the specified path. | ||
| case file(path: String) | ||
| } | ||
|
|
||
| /// Return the version of the embedded libsqlite3 as a 32-bit integer value. | ||
| public static func libraryVersion() -> Int32 { | ||
| sqlite_nio_sqlite3_libversion_number() | ||
| } | ||
|
|
||
| /// Return the version of the embedded libsqlite3 as a string. | ||
| public static func libraryVersionString() -> String { | ||
| String(cString: sqlite_nio_sqlite3_libversion()) | ||
| } | ||
|
|
||
| // See `SQLiteDatabase.logger`. | ||
| public let logger: Logger | ||
|
|
||
| /// The underlying `sqlite3` connection handle. | ||
| let handle: SQLiteConnectionHandle | ||
|
|
||
| /// Initialize a new ``SQLiteConnection``. Internal use only. | ||
| private init(handle: OpaquePointer?, logger: Logger) { | ||
| self.handle = .init(handle) | ||
| self.logger = logger | ||
| } | ||
|
|
||
| /// Returns the most recent error message from the connection as a string. | ||
| var errorMessage: String? { | ||
| sqlite_nio_sqlite3_errmsg(self.handle.raw).map { String(cString: $0) } | ||
| } | ||
|
|
||
| /// `false` if the connection is valid, `true` if not. | ||
| public var isClosed: Bool { | ||
| self.handle.raw == nil | ||
| } | ||
|
|
||
| /// Open a new connection to an SQLite database. | ||
| /// | ||
| /// - Parameters: | ||
| /// - storage: Specifies the location of the database for the connection. See ``Storage`` for details. | ||
| /// - logger: The logger used by the connection. Defaults to a new `Logger`. | ||
| /// - Returns: A new connection object. | ||
| public static func open( | ||
| storage: Storage = .memory, | ||
| logger: Logger = .init(label: "codes.vapor.sqlite") | ||
| ) async throws -> SQLiteConnection { | ||
| let path: String | ||
| switch storage { | ||
| case .memory: path = ":memory:" | ||
| case .file(let file): path = file | ||
| } | ||
|
|
||
| var handle: OpaquePointer? | ||
| let openOptions = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI | SQLITE_OPEN_EXRESCODE | ||
| let openRet = sqlite_nio_sqlite3_open_v2(path, &handle, openOptions, nil) | ||
| guard openRet == SQLITE_OK else { | ||
| throw SQLiteError(reason: .init(statusCode: openRet), message: "Failed to open to SQLite database at \(path)") | ||
| } | ||
|
|
||
| let busyRet = sqlite_nio_sqlite3_busy_handler(handle, { _, _ in 1 }, nil) | ||
| guard busyRet == SQLITE_OK else { | ||
| sqlite_nio_sqlite3_close(handle) | ||
| throw SQLiteError(reason: .init(statusCode: busyRet), message: "Failed to set busy handler for SQLite database at \(path)") | ||
| } | ||
|
|
||
| logger.debug("Connected to sqlite database", metadata: ["path": .string(path)]) | ||
| return SQLiteConnection(handle: handle, logger: logger) | ||
| } | ||
|
|
||
| /// Returns the last value generated by auto-increment functionality on this database. | ||
| public func lastAutoincrementID() async throws -> Int { | ||
| numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw)) | ||
| } | ||
|
|
||
| /// Run the provided closure with this connection. | ||
| public func withConnection<T>( | ||
| _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T | ||
| ) async throws -> T { | ||
| try await closure(self) | ||
| } | ||
|
|
||
| // See `SQLiteDatabase.query(_:_:logger:_:)`. | ||
| public func query( | ||
| _ query: String, | ||
| _ binds: [SQLiteData], | ||
| logger: Logger, | ||
| _ onRow: @escaping @Sendable (SQLiteRow) -> Void | ||
| ) async throws { | ||
| var statement = try SQLiteStatement(query: query, on: self) | ||
| let columns = try statement.columns() | ||
| try statement.bind(binds) | ||
| while let row = try statement.nextRow(for: columns) { | ||
| onRow(row) | ||
| } | ||
| } | ||
|
|
||
| /// Close the connection and invalidate its handle. | ||
| public func close() async throws { | ||
| sqlite_nio_sqlite3_close(self.handle.raw) | ||
| self.handle.raw = nil | ||
| } | ||
|
|
||
| /// Install the provided ``SQLiteCustomFunction`` on the connection. | ||
| public func install(customFunction: SQLiteCustomFunction) async throws { | ||
| self.logger.trace("Adding custom function \(customFunction.name)") | ||
| try customFunction.install(in: self) | ||
| } | ||
|
|
||
| /// Uninstall the provided ``SQLiteCustomFunction`` from the connection. | ||
| public func uninstall(customFunction: SQLiteCustomFunction) async throws { | ||
| self.logger.trace("Removing custom function \(customFunction.name)") | ||
| try customFunction.uninstall(in: self) | ||
| } | ||
|
|
||
| deinit { | ||
| assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing") | ||
| } | ||
| } | ||
| #endif // NativeConcurrency |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self, before moving this out of draft, I need to research avoiding the need for a separate/new package.swift file here