Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions Package@swift-6.1.swift
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"),
] }
4 changes: 3 additions & 1 deletion Plugins/VendorSQLite/VendorSQLite3.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ struct VendorSQLite: CommandPlugin {
static let sqliteURL = URL(string: "https://sqlite.org")!
static let vendorPrefix = "sqlite_nio"

static var verbose = false
// `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 }

Expand Down
4 changes: 4 additions & 0 deletions Sources/SQLiteNIO/Exports.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// These re-exports are all SwiftNIO types, which are elided on the NativeConcurrency build (the
// NIO-free path uses Swift Concurrency + `[UInt8]` instead of EventLoop/ByteBuffer).
#if !NativeConcurrency
@_documentation(visibility: internal) @_exported import struct NIOCore.ByteBuffer

// See SQLiteConnection.swift for why we gate on `os(WASI)` rather than
Expand All @@ -19,3 +22,4 @@
#else
@_documentation(visibility: internal) @_exported import class NIOPosix.MultiThreadedEventLoopGroup
#endif
#endif // !NativeConcurrency
74 changes: 74 additions & 0 deletions Sources/SQLiteNIO/NativeConcurrencyLock.swift
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
3 changes: 3 additions & 0 deletions Sources/SQLiteNIO/SQLiteConnection+Hooks.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if !NativeConcurrency // NIO-based hooks; the NativeConcurrency build uses SQLiteConnection+NativeConcurrency.swift (no hooks surface)
import Foundation
import NIOConcurrencyHelpers
import NIOCore
Expand Down Expand Up @@ -865,3 +866,5 @@ extension SQLiteConnection {
self.observerBuckets.withLockedValue { $0 = .init() }
}
}

#endif // !NativeConcurrency
154 changes: 154 additions & 0 deletions Sources/SQLiteNIO/SQLiteConnection+NativeConcurrency.swift
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
Loading