diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 7810eff..14c39b4 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,5 +1,4 @@
version: 2
-enable-beta-ecosystems: true
updates:
- package-ecosystem: "github-actions"
directory: "/"
@@ -11,14 +10,3 @@ updates:
dependencies:
patterns:
- "*"
- - package-ecosystem: "swift"
- directory: "/"
- schedule:
- interval: "daily"
- open-pull-requests-limit: 6
- allow:
- - dependency-type: all
- groups:
- all-dependencies:
- patterns:
- - "*"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ce62c58..92622a6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,7 +16,7 @@ jobs:
dependents-check:
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
- container: swift:5.9-jammy
+ container: swift:5.10-jammy
steps:
- name: Check out package
uses: actions/checkout@v4
@@ -38,3 +38,4 @@ jobs:
unit-tests:
uses: vapor/ci/.github/workflows/run-unit-tests.yml@main
+ secrets: inherit
\ No newline at end of file
diff --git a/Package.swift b/Package.swift
index d421ea7..d7d6fe4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.7
+// swift-tools-version:5.8
import PackageDescription
let package = Package(
@@ -13,8 +13,8 @@ let package = Package(
.library(name: "SQLiteNIO", targets: ["SQLiteNIO"]),
],
dependencies: [
- .package(url: "https://github.com/apple/swift-nio.git", from: "2.58.0"),
- .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
+ .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
],
targets: [
.plugin(
@@ -25,38 +25,69 @@ let package = Package(
),
exclude: ["001-warnings-and-data-race.patch"]
),
- .target(name: "CSQLite", cSettings: [
- // Derived from sqlite3 version 3.43.0
- .define("SQLITE_DQS", to: "0"),
- .define("SQLITE_ENABLE_API_ARMOR"),
- .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_MEMORY_MANAGEMENT"),
- .define("SQLITE_ENABLE_PREUPDATE_HOOK"),
- .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_DEPRECATED"),
- .define("SQLITE_OMIT_LOAD_EXTENSION"),
- .define("SQLITE_OMIT_SHARED_CACHE"),
- .define("SQLITE_SECURE_DELETE"),
- .define("SQLITE_THREADSAFE", to: "2"),
- .define("SQLITE_USE_URI"),
- ]),
- .target(name: "SQLiteNIO", dependencies: [
- .target(name: "CSQLite"),
- .product(name: "Logging", package: "swift-log"),
- .product(name: "NIO", package: "swift-nio"),
- ]),
- .testTarget(name: "SQLiteNIOTests", dependencies: ["SQLiteNIO"]),
+ .target(
+ name: "CSQLite",
+ cSettings: sqliteCSettings
+ ),
+ .target(
+ name: "SQLiteNIO",
+ dependencies: [
+ .target(name: "CSQLite"),
+ .product(name: "Logging", package: "swift-log"),
+ .product(name: "NIOCore", package: "swift-nio"),
+ .product(name: "NIOPosix", package: "swift-nio"),
+ .product(name: "NIOFoundationCompat", package: "swift-nio"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "SQLiteNIOTests",
+ dependencies: [
+ .target(name: "SQLiteNIO"),
+ ],
+ swiftSettings: swiftSettings
+ ),
]
)
+
+var swiftSettings: [SwiftSetting] { [
+ .enableUpcomingFeature("ConciseMagicFile"),
+ .enableUpcomingFeature("ForwardTrailingClosures"),
+] }
+
+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_AUTHORIZATION"),
+ .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"),
+ .define("SQLITE_UNTESTABLE"),
+ .define("SQLITE_USE_URI"),
+] }
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..9aa260b
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,99 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let package = Package(
+ name: "sqlite-nio",
+ platforms: [
+ .macOS(.v10_15),
+ .iOS(.v13),
+ .watchOS(.v6),
+ .tvOS(.v13),
+ ],
+ products: [
+ .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-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"),
+ .product(name: "NIOCore", package: "swift-nio"),
+ .product(name: "NIOPosix", package: "swift-nio"),
+ .product(name: "NIOFoundationCompat", package: "swift-nio"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "SQLiteNIOTests",
+ dependencies: [
+ .target(name: "SQLiteNIO"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ ]
+)
+
+var swiftSettings: [SwiftSetting] { [
+ .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_AUTHORIZATION"),
+ .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"),
+ .define("SQLITE_UNTESTABLE"),
+ .define("SQLITE_USE_URI"),
+] }
diff --git a/README.md b/README.md
index 9c7e5ce..e191d80 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,9 @@
-
-
-
+
+
+
diff --git a/Sources/SQLiteNIO/Docs.docc/Documentation.md b/Sources/SQLiteNIO/Docs.docc/Documentation.md
index 4ef954f..780d865 100644
--- a/Sources/SQLiteNIO/Docs.docc/Documentation.md
+++ b/Sources/SQLiteNIO/Docs.docc/Documentation.md
@@ -4,12 +4,11 @@
@TitleHeading(Package)
}
-🪶 Non-blocking, event-driven Swift client for SQLite with embedded libsqlite
+🪶 Non-blocking, event-driven Swift client for SQLite with embedded `libsqlite`.
## Supported Versions
-This package is compatible with all platforms supported by [SwiftNIO 2.x](https://github.com/apple/swift-nio/). It has
-been specifically tested on the following platforms:
+This package is compatible with all platforms supported by [SwiftNIO 2.x](https://github.com/apple/swift-nio/). It has been specifically tested on the following platforms:
- Ubuntu 20.04 ("Focal") and 22.04 ("Jammy")
- Amazon Linux 2
diff --git a/Sources/SQLiteNIO/Docs.docc/images/vapor-sqlitenio-logo.svg b/Sources/SQLiteNIO/Docs.docc/Resources/vapor-sqlitenio-logo.svg
similarity index 100%
rename from Sources/SQLiteNIO/Docs.docc/images/vapor-sqlitenio-logo.svg
rename to Sources/SQLiteNIO/Docs.docc/Resources/vapor-sqlitenio-logo.svg
diff --git a/Sources/SQLiteNIO/Docs.docc/theme-settings.json b/Sources/SQLiteNIO/Docs.docc/theme-settings.json
index 2266669..640f319 100644
--- a/Sources/SQLiteNIO/Docs.docc/theme-settings.json
+++ b/Sources/SQLiteNIO/Docs.docc/theme-settings.json
@@ -1,6 +1,6 @@
{
"theme": {
- "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" },
+ "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
"border-radius": "0",
"button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
"code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
diff --git a/Sources/SQLiteNIO/Exports.swift b/Sources/SQLiteNIO/Exports.swift
index 65b24ee..0542622 100644
--- a/Sources/SQLiteNIO/Exports.swift
+++ b/Sources/SQLiteNIO/Exports.swift
@@ -1,17 +1,5 @@
-#if swift(>=5.8)
-
@_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
-
-@_exported import struct NIOCore.ByteBuffer
-@_exported import class NIOPosix.NIOThreadPool
-@_exported import protocol NIOCore.EventLoop
-@_exported import protocol NIOCore.EventLoopGroup
-@_exported import class NIOPosix.MultiThreadedEventLoopGroup
-
-#endif
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 62543b5..2b0f306 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -1,103 +1,114 @@
import NIOCore
+#if swift(<5.9)
+import NIOConcurrencyHelpers
+#endif
import NIOPosix
import CSQLite
import Logging
-public protocol SQLiteDatabase {
- var logger: Logger { get }
- var eventLoop: any EventLoop { get }
-
- @preconcurrency func query(
- _ query: String,
- _ binds: [SQLiteData],
- logger: Logger,
- _ onRow: @escaping @Sendable (SQLiteRow) -> Void
- ) -> EventLoopFuture
-
- @preconcurrency func withConnection(
- _: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
- ) -> EventLoopFuture
-}
-
-extension SQLiteDatabase {
- @preconcurrency
- public func query(
- _ query: String,
- _ binds: [SQLiteData] = [],
- _ onRow: @escaping @Sendable (SQLiteRow) -> Void
- ) -> EventLoopFuture {
- self.query(query, binds, logger: self.logger, onRow)
- }
-
- public func query(
- _ query: String,
- _ binds: [SQLiteData] = []
- ) -> EventLoopFuture<[SQLiteRow]> {
- let rows: UnsafeMutableTransferBox<[SQLiteRow]> = .init([])
- return self.query(query, binds, logger: self.logger) { row in
- rows.wrappedValue.append(row)
- }.map { rows.wrappedValue }
- }
- }
-
-extension SQLiteDatabase {
- public func logging(to logger: Logger) -> any SQLiteDatabase {
- _SQLiteDatabaseCustomLogger(database: self, logger: logger)
+/// A wrapper for the `OpaquePointer` used to represent an open `sqlite3` handle.
+///
+/// This wrapper serves two purposes:
+///
+/// - Silencing `Sendable` warnings relating to use of the pointer, and
+/// - Preventing confusion with other C types which import as opaque pointers.
+///
+/// The use of `@unchecked Sendable` is safe for this type because:
+///
+/// - We ensure that access to the raw handle only ever takes place while running on an `NIOThreadPool`.
+/// This does not prevent concurrent access to the handle from multiple threads, but does tend to limit
+/// the possibility of misuse (and of course prevents CPU-bound work from ending up on an event loop).
+/// - The embedded SQLite is built with `SQLITE_THREADSAFE=1` (serialized mode, permitting safe use of a
+/// given connection handle simultaneously from multiple threads).
+/// - We include `SQLITE_OPEN_FULLMUTEX` when calling `sqlite_open_v2()`, guaranteeing the use of the
+/// serialized threading mode for each connection even if someone uses `sqlite3_config()` to make the
+/// less strict multithreaded mode the default.
+///
+/// And finally, the use of `@unchecked` in particular is justified because:
+///
+/// 1. We need to be able to mutate the value in order to make it `nil` when the connection it represented
+/// is closed. We use the `nil` value as a sentinel by which we determine a connection's validity. Also,
+/// _not_ `nil`-ing it out would leave a dangling/freed pointer in place, which is just begging for a
+/// segfault.
+/// 2. An `OpaquePointer` can not be natively `Sendable`, by definition; it's opaque! The `@unchecked`
+/// annotation is how we tell the compiler "we've taken the appropriate precautions to make moving
+/// values of this type between isolation regions safe".
+///
+/// > Note: It appears that in Swift 5.8, TSan likes to throw false positive warnings about this type, hence
+/// > the compiler conditionals around using bogus extra locking.
+final class SQLiteConnectionHandle: @unchecked Sendable {
+ #if swift(<5.9)
+ private let _raw: NIOLockedValueBox
+ var raw: OpaquePointer? {
+ get { self._raw.withLockedValue { $0 } }
+ set { self._raw.withLockedValue { $0 = newValue } }
}
-}
-
-private struct _SQLiteDatabaseCustomLogger: SQLiteDatabase {
- let database: any SQLiteDatabase
- var eventLoop: any EventLoop {
- self.database.eventLoop
- }
- let logger: Logger
- @preconcurrency func withConnection(
- _ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
- ) -> EventLoopFuture {
- self.database.withConnection(closure)
- }
-
- @preconcurrency func query(
- _ query: String,
- _ binds: [SQLiteData],
- logger: Logger,
- _ onRow: @escaping @Sendable (SQLiteRow) -> Void
- ) -> EventLoopFuture {
- self.database.query(query, binds, logger: logger, onRow)
+ init(_ raw: OpaquePointer?) {
+ self._raw = .init(raw)
}
-}
-
-internal final class SQLiteConnectionHandle: @unchecked Sendable {
+ #else
var raw: OpaquePointer?
init(_ raw: OpaquePointer?) {
self.raw = raw
}
+ #endif
}
-public final class SQLiteConnection: SQLiteDatabase {
- /// Available SQLite storage methods.
- public enum Storage {
- /// In-memory storage. Not persisted between application launches.
- /// Good for unit testing or caching.
+/// Represents a single open connection to an SQLite database, either on disk or in memory.
+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.
+ ///
+ /// In-memory databases persist only so long as the connection to them is open, and are not shared
+ /// between processes. In addition, because this package builds the sqlite3 amalgamation with the
+ /// recommended `SQLITE_OMIT_SHARED_CACHE` option, it is not possible to open multiple connections
+ /// to a single in-memory database; use a temporary file instead.
+ ///
+ /// In-memory databases are useful for unit testing or caching purposes.
case memory
- /// File-based storage, persisted between application launches.
+ /// An SQLite database stored in a file at the specified path.
+ ///
+ /// If a relative path is specified, it is interpreted relative to the current working directory of the
+ /// current process (e.g. `NIOFileSystem.shared.currentWorkingDirectory`) at the time of establishing
+ /// the connection. It is strongly recommended that users always use absolute paths whenever possible.
+ ///
+ /// File-based databases persist as long as the files representing them on disk does, and can be opened
+ /// multiple times within the same process or even by multiple processes if configured properly.
case file(path: String)
}
- public let eventLoop: any EventLoop
-
- internal let handle: SQLiteConnectionHandle
- internal let threadPool: NIOThreadPool
- public let logger: Logger
+ /// Return the version of the embedded libsqlite3 as a 32-bit integer value.
+ ///
+ /// The value is laid out identicallly to [the `SQLITE_VERSION_NUMBER` constant](c_source_id).
+ ///
+ /// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html
+ public static func libraryVersion() -> Int32 {
+ sqlite_nio_sqlite3_libversion_number()
+ }
- public var isClosed: Bool {
- self.handle.raw == nil
+ /// Return the version of the embedded libsqlite3 as a string.
+ ///
+ /// The string is formatted identically to [the `SQLITE_VERSION` constant](c_source_id).
+ ///
+ /// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html
+ public static func libraryVersionString() -> String {
+ String(cString: sqlite_nio_sqlite3_libversion())
}
-
+
+ /// Open a new connection to an SQLite database.
+ ///
+ /// This is equivalent to invoking ``open(storage:threadPool:logger:on:)-64n3x`` using the
+ /// `NIOThreadPool` and `MultiThreadedEventLoopGroup` singletons. This is the recommended configuration
+ /// for all users.
+ ///
+ /// - 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 future whose value on success is a new connection object.
public static func open(
storage: Storage = .memory,
logger: Logger = .init(label: "codes.vapor.sqlite")
@@ -109,42 +120,71 @@ public final class SQLiteConnection: SQLiteDatabase {
on: MultiThreadedEventLoopGroup.singleton.any()
)
}
-
+
+ /// Open a new connection to an SQLite database.
+ ///
+ /// - Parameters:
+ /// - storage: Specifies the location of the database for the connection. See ``Storage`` for details.
+ /// - threadPool: An `NIOThreadPool` used to execute all libsqlite3 API calls for this connection.
+ /// - logger: The logger used by the connection. Defaults to a new `Logger`.
+ /// - eventLoop: An `EventLoop` to associate with the connection for creating futures.
+ /// - Returns: A future whose value on success is a new connection object.
public static func open(
storage: Storage = .memory,
threadPool: NIOThreadPool,
logger: Logger = .init(label: "codes.vapor.sqlite"),
on eventLoop: any EventLoop
) -> EventLoopFuture {
+ threadPool.runIfActive(eventLoop: eventLoop) {
+ try self.openInternal(storage: storage, threadPool: threadPool, logger: logger, eventLoop: eventLoop)
+ }
+ }
+
+ /// The underlying implementation of ``open(storage:threadPool:logger:on:)-64n3x`` and
+ /// ``open(storage:threadPool:logger:on:)-3m3lb``.
+ private static func openInternal(
+ storage: Storage,
+ threadPool: NIOThreadPool,
+ logger: Logger,
+ eventLoop: any EventLoop
+ ) throws -> SQLiteConnection {
let path: String
switch storage {
- case .memory:
- path = ":memory:"
- case .file(let file):
- path = file
+ case .memory: path = ":memory:"
+ case .file(let file): path = file
}
- return threadPool.runIfActive(eventLoop: eventLoop) {
- var handle: OpaquePointer?
- let options = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI
-
- if sqlite_nio_sqlite3_open_v2(path, &handle, options, nil) == SQLITE_OK, sqlite_nio_sqlite3_busy_handler(handle, { _, _ in 1 }, nil) == SQLITE_OK {
- let connection = SQLiteConnection(
- handle: handle,
- threadPool: threadPool,
- logger: logger,
- on: eventLoop
- )
- logger.debug("Connected to sqlite db: \(path)")
- return connection
- } else {
- logger.error("Failed to connect to sqlite db: \(path)")
- throw SQLiteError(reason: .cantOpen, message: "Cannot open SQLite database: \(storage)")
- }
+ 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, threadPool: threadPool, logger: logger, on: eventLoop)
}
- init(
+ // See `SQLiteDatabase.eventLoop`.
+ public let eventLoop: any EventLoop
+
+ // See `SQLiteDatabase.logger`.
+ public let logger: Logger
+
+ /// The underlying `sqlite3` connection handle.
+ let handle: SQLiteConnectionHandle
+
+ /// The thread pool used by this connection when calling libsqlite3 APIs.
+ private let threadPool: NIOThreadPool
+
+ /// Initialize a new ``SQLiteConnection``. Internal use only.
+ private init(
handle: OpaquePointer?,
threadPool: NIOThreadPool,
logger: Logger,
@@ -156,36 +196,41 @@ public final class SQLiteConnection: SQLiteDatabase {
self.eventLoop = eventLoop
}
- public static func libraryVersion() -> Int32 {
- sqlite_nio_sqlite3_libversion_number()
+ /// Returns the most recent error message from the connection as a string.
+ ///
+ /// This is only valid until another operation is performed on the connection; watch out for races.
+ var errorMessage: String? {
+ sqlite_nio_sqlite3_errmsg(self.handle.raw).map { String(cString: $0) }
}
- public static func libraryVersionString() -> String {
- String(cString: sqlite_nio_sqlite3_libversion())
+ /// `false` if the connection is valid, `true` if not.
+ public var isClosed: Bool {
+ self.handle.raw == nil
}
-
+
+ /// Returns the last value generated by auto-increment functionality (either the version implied by
+ /// `INTEGER PRIMARY KEY` or that of the explicit `AUTO_INCREMENT` modifier) on this database.
+ ///
+ /// Only valid until the next operation is performed on the connection; watch out for races.
+ ///
+ /// - Returns: A future containing the most recently inserted rowid value.
public func lastAutoincrementID() -> EventLoopFuture {
self.threadPool.runIfActive(eventLoop: self.eventLoop) {
- let rowid = sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw)
- return numericCast(rowid)
+ numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
}
}
- internal var errorMessage: String? {
- if let raw = sqlite_nio_sqlite3_errmsg(self.handle.raw) {
- return String(cString: raw)
- } else {
- return nil
- }
- }
-
- @preconcurrency public func withConnection(
+ // See `SQLiteDatabase.withConnection(_:)`.
+ @preconcurrency
+ public func withConnection(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
) -> EventLoopFuture {
closure(self)
}
- @preconcurrency public func query(
+ // See `SQLiteDatabase.query(_:_:logger:_:)`.
+ @preconcurrency
+ public func query(
_ query: String,
_ binds: [SQLiteData],
logger: Logger,
@@ -201,7 +246,7 @@ public final class SQLiteConnection: SQLiteDatabase {
}
var futures: [EventLoopFuture] = []
do {
- let statement = try SQLiteStatement(query: query, on: self)
+ 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) {
@@ -215,36 +260,143 @@ public final class SQLiteConnection: SQLiteDatabase {
return promise.futureResult
}
+ /// Close the connection and invalidate its handle.
+ ///
+ /// No further operations may be performed on the connection after calling this method.
+ ///
+ /// - Returns: A future indicating completion of connection closure.
public func close() -> EventLoopFuture {
self.threadPool.runIfActive(eventLoop: self.eventLoop) {
sqlite_nio_sqlite3_close(self.handle.raw)
self.handle.raw = nil
}
}
-
+
+ /// Install the provided ``SQLiteCustomFunction`` on the connection.
+ ///
+ /// - Parameter customFunction: The function to install.
+ /// - Returns: A future indicating completion of the install operation.
public func install(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
- logger.trace("Adding custom function \(customFunction.name)")
+ self.logger.trace("Adding custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
try customFunction.install(in: self)
}
}
+ /// Uninstall the provided ``SQLiteCustomFunction`` from the connection.
+ ///
+ /// - Parameter customFunction: The function to remove.
+ /// - Returns: A future indicating completion of the uninstall operation.
public func uninstall(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
- logger.trace("Removing custom function \(customFunction.name)")
+ self.logger.trace("Removing custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
try customFunction.uninstall(in: self)
}
}
+ /// Deinitializer for ``SQLiteConnection``.
deinit {
assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing")
}
}
-fileprivate final class UnsafeMutableTransferBox: @unchecked Sendable {
- var wrappedValue: Wrapped
- init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue }
-}
+extension SQLiteConnection {
+ /// Open a new connection to an SQLite database.
+ ///
+ /// This is equivalent to invoking ``open(storage:threadPool:logger:on:)-3m3lb`` using the
+ /// `NIOThreadPool` and `MultiThreadedEventLoopGroup` singletons. This is the recommended configuration
+ /// for all users.
+ ///
+ /// - 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 future whose value on success is a new connection object.
+ public static func open(
+ storage: Storage = .memory,
+ logger: Logger = .init(label: "codes.vapor.sqlite")
+ ) async throws -> SQLiteConnection {
+ try await Self.open(
+ storage: storage,
+ threadPool: NIOThreadPool.singleton,
+ logger: logger,
+ on: MultiThreadedEventLoopGroup.singleton.any()
+ )
+ }
+
+ /// Open a new connection to an SQLite database.
+ ///
+ /// - Parameters:
+ /// - storage: Specifies the location of the database for the connection. See ``Storage`` for details.
+ /// - threadPool: An `NIOThreadPool` used to execute all libsqlite3 API calls for this connection.
+ /// - logger: The logger used by the connection. Defaults to a new `Logger`.
+ /// - eventLoop: An `EventLoop` to associate with the connection for creating futures.
+ /// - Returns: A new connection object.
+ public static func open(
+ storage: Storage = .memory,
+ threadPool: NIOThreadPool,
+ logger: Logger = .init(label: "codes.vapor.sqlite"),
+ on eventLoop: any EventLoop
+ ) async throws -> SQLiteConnection {
+ try await threadPool.runIfActive {
+ try self.openInternal(storage: storage, threadPool: threadPool, logger: logger, eventLoop: eventLoop)
+ }
+ }
+
+ /// Returns the last value generated by auto-increment functionality (either the version implied by
+ /// `INTEGER PRIMARY KEY` or that of the explicit `AUTO_INCREMENT` modifier) on this database.
+ ///
+ /// Only valid until the next operation is performed on the connection; watch out for races.
+ ///
+ /// - Returns: The most recently inserted rowid value.
+ public func lastAutoincrementID() async throws -> Int {
+ try await self.threadPool.runIfActive {
+ numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
+ }
+ }
+
+ /// Concurrency-aware variant of ``withConnection(_:)-8cmxp``.
+ public func withConnection(
+ _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
+ ) async throws -> T {
+ try await closure(self)
+ }
+
+ /// Concurrency-aware variant of ``query(_:_:_:)-etrj``.
+ public func query(
+ _ query: String,
+ _ binds: [SQLiteData],
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) async throws {
+ try await self.query(query, binds, onRow).get()
+ }
+
+ /// Close the connection and invalidate its handle.
+ ///
+ /// No further operations may be performed on the connection after calling this method.
+ public func close() async throws {
+ try await self.threadPool.runIfActive {
+ sqlite_nio_sqlite3_close(self.handle.raw)
+ self.handle.raw = nil
+ }
+ }
-extension SQLiteConnection: Sendable {}
-extension SQLiteConnection.Storage: Sendable {}
+ /// Install the provided ``SQLiteCustomFunction`` on the connection.
+ ///
+ /// - Parameter customFunction: The function to install.
+ public func install(customFunction: SQLiteCustomFunction) async throws {
+ self.logger.trace("Adding custom function \(customFunction.name)")
+ return try await self.threadPool.runIfActive {
+ try customFunction.install(in: self)
+ }
+ }
+
+ /// Uninstall the provided ``SQLiteCustomFunction`` from the connection.
+ ///
+ /// - Parameter customFunction: The function to remove.
+ public func uninstall(customFunction: SQLiteCustomFunction) async throws {
+ self.logger.trace("Removing custom function \(customFunction.name)")
+ return try await self.threadPool.runIfActive {
+ try customFunction.uninstall(in: self)
+ }
+ }
+}
diff --git a/Sources/SQLiteNIO/SQLiteCustomFunction.swift b/Sources/SQLiteNIO/SQLiteCustomFunction.swift
index b9a58f0..64e990d 100644
--- a/Sources/SQLiteNIO/SQLiteCustomFunction.swift
+++ b/Sources/SQLiteNIO/SQLiteCustomFunction.swift
@@ -24,10 +24,12 @@ public final class SQLiteCustomFunction: Hashable {
/// The name of the SQL function
public var name: String { identity.name }
+
private let identity: Identity
private let pure: Bool
+ private let indirect: Bool
private let kind: Kind
- private var eTextRep: Int32 { (SQLITE_UTF8 | (pure ? SQLITE_DETERMINISTIC : 0)) }
+ private var eTextRep: Int32 { (SQLITE_UTF8 | (pure ? SQLITE_DETERMINISTIC : 0) | (indirect ? 0 : SQLITE_DIRECTONLY)) }
public struct SQLiteCustomFunctionArgumentError: Error {
public let count: Int
@@ -38,19 +40,19 @@ public final class SQLiteCustomFunction: Hashable {
_ name: String,
argumentCount: Int32? = nil,
pure: Bool = false,
+ indirect: Bool = false,
function: @Sendable @escaping ([SQLiteData]) throws -> (any SQLiteDataConvertible)?)
{
self.identity = Identity(name: name, nArg: argumentCount ?? -1)
self.pure = pure
+ self.indirect = indirect
self.kind = .function { (argc, argv) in
- let count = Int(argc)
- let arguments = try (0 ..< count).map { index -> SQLiteData in
+ try function((0 ..< Int(argc)).map { index -> SQLiteData in
guard let value = argv?[index] else {
- throw SQLiteCustomFunctionArgumentError(count: count, index: index)
+ throw SQLiteCustomFunctionArgumentError(count: Int(argc), index: index)
}
return try SQLiteData(sqliteValue: value)
- }
- return try function(arguments)
+ })
}
}
@@ -95,10 +97,12 @@ public final class SQLiteCustomFunction: Hashable {
_ name: String,
argumentCount: Int32? = nil,
pure: Bool = false,
+ indirect: Bool = false,
aggregate: Aggregate.Type
) {
self.identity = Identity(name: name, nArg: argumentCount ?? -1)
self.pure = pure
+ self.indirect = indirect
self.kind = .aggregate { Aggregate() }
}
@@ -106,22 +110,17 @@ public final class SQLiteCustomFunction: Hashable {
/// See https://sqlite.org/c3ref/create_function.html
func install(in connection: SQLiteConnection) throws {
// Retain the function definition
- let definition = kind.definition
- let definitionP = Unmanaged.passRetained(definition).toOpaque()
-
let code = sqlite_nio_sqlite3_create_function_v2(
connection.handle.raw,
- identity.name,
- identity.nArg,
- eTextRep,
- definitionP,
- kind.xFunc,
- kind.xStep,
- kind.xFinal,
- { definitionP in
- // Release the function definition
- Unmanaged.fromOpaque(definitionP!).release()
- })
+ self.identity.name,
+ self.identity.nArg,
+ self.eTextRep,
+ Unmanaged.passRetained(self.kind.definition).toOpaque(),
+ self.kind.xFunc,
+ self.kind.xStep,
+ self.kind.xFinal,
+ { Unmanaged.fromOpaque($0!).release() } // Release the function definition
+ )
guard code == SQLITE_OK else {
throw SQLiteError(statusCode: code, connection: connection)
@@ -133,10 +132,11 @@ public final class SQLiteCustomFunction: Hashable {
func uninstall(in connection: SQLiteConnection) throws {
let code = sqlite_nio_sqlite3_create_function_v2(
connection.handle.raw,
- identity.name,
- identity.nArg,
- eTextRep,
- nil, nil, nil, nil, nil)
+ self.identity.name,
+ self.identity.nArg,
+ self.eTextRep,
+ nil, nil, nil, nil, nil
+ )
guard code == SQLITE_OK else {
throw SQLiteError(statusCode: code, connection: connection)
diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift
index b93a91f..adf7588 100644
--- a/Sources/SQLiteNIO/SQLiteData.swift
+++ b/Sources/SQLiteNIO/SQLiteData.swift
@@ -1,23 +1,30 @@
import CSQLite
import NIOCore
-/// Supported SQLite data types.
-public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
- /// `Int`.
+/// 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)
- /// `Double`.
+ /// `REAL` affinity, represented in Swift by `Double`.
case float(Double)
- /// `String`.
+ /// `TEXT` affinity, represented in Swift by `String`.
case text(String)
- /// `ByteBuffer`.
+ /// `BLOB` affinity, represented in Swift by `ByteBuffer`.
case blob(ByteBuffer)
- /// `NULL`.
+ /// A `NULL` value.
case null
+ /// Returns the integer value of the data, performing conversions where possible.
+ ///
+ /// 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? {
switch self {
case .integer(let integer):
@@ -31,6 +38,10 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
+ /// Returns the real number value of the data, performing conversions where possible.
+ ///
+ /// If the data has `INTEGER` or `TEXT` affinity, an attempt is made to interpret the value as a `Double`. `BLOB`
+ /// and `NULL` values always return `nil`.
public var double: Double? {
switch self {
case .integer(let integer):
@@ -44,6 +55,10 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
+ /// Returns the textual value of the data, performing conversions where possible.
+ ///
+ /// If the data has `INTEGER` or `REAL` affinity, the value is converted to text. `BLOB` and `NULL` values always
+ /// return `nil`.
public var string: String? {
switch self {
case .integer(let integer):
@@ -57,6 +72,10 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
+ /// Returns the boolean value of the data, where possible.
+ ///
+ /// Returns `true` if the value of ``integer`` is exactly `1`, `false` if the value of ``integer`` is exactly
+ /// `0`, or `nil` for all other cases.
public var bool: Bool? {
switch self.integer {
case 1: return true
@@ -65,6 +84,9 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
+ /// Returns the data as a blob, if it has `BLOB` affinity.
+ ///
+ /// `INTEGER`, `REAL`, `TEXT`, and `NULL` values always return `nil`.
public var blob: ByteBuffer? {
switch self {
case .blob(let buffer):
@@ -74,6 +96,7 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
+ /// `true` if the value is `NULL`, `false` otherwise.
public var isNull: Bool {
switch self {
case .null:
@@ -83,33 +106,32 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
- /// Description of data
+ // See `CustomStringConvertible.description`.
public var description: String {
switch self {
case .blob(let data): return "<\(data.readableBytes) bytes>"
case .float(let float): return float.description
case .integer(let int): return int.description
case .null: return "null"
- case .text(let text): return "\"" + text + "\""
+ case .text(let text): return #""\#(text)""#
}
}
- /// See `Encodable`.
+ // See `Encodable.encode(to:)`.
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integer(let value): try container.encode(value)
case .float(let value): try container.encode(value)
case .text(let value): try container.encode(value)
- case .blob(var value):
- let bytes = value.readBytes(length: value.readableBytes) ?? []
- try container.encode(bytes)
+ case .blob(let value): try container.encode(Array(value.readableBytesView)) // N.B.: Don't use ByteBuffer's Codable conformance; it encodes as Base64, not raw bytes
case .null: try container.encodeNil()
}
}
}
extension SQLiteData {
+ /// Attempt to interpret an `sqlite3_value` as an equivalent ``SQLiteData``.
init(sqliteValue: OpaquePointer) throws {
switch sqlite_nio_sqlite3_value_type(sqliteValue) {
case SQLITE_NULL:
@@ -119,7 +141,11 @@ extension SQLiteData {
case SQLITE_FLOAT:
self = .float(sqlite_nio_sqlite3_value_double(sqliteValue))
case SQLITE_TEXT:
- self = .text(String(cString: sqlite_nio_sqlite3_value_text(sqliteValue)!))
+ if let raw = sqlite_nio_sqlite3_value_text(sqliteValue) {
+ self = .text(String.init(cString: raw))
+ } else {
+ self = .text("")
+ }
case SQLITE_BLOB:
if let bytes = sqlite_nio_sqlite3_value_blob(sqliteValue) {
let count = Int(sqlite_nio_sqlite3_value_bytes(sqliteValue))
@@ -133,9 +159,10 @@ extension SQLiteData {
}
}
+ /// The error thrown by ``init(sqliteValue:)`` if an `sqlite3_value` has an unknown type.
+ ///
+ /// This should never happen, and this error should not have been made `public`.
public struct SQLiteCustomFunctionUnexpectedValueTypeError: Error {
public let type: Int32
}
}
-
-extension SQLiteData: Sendable {}
diff --git a/Sources/SQLiteNIO/SQLiteDataConvertible.swift b/Sources/SQLiteNIO/SQLiteDataConvertible.swift
index f6af2c7..2f788c1 100644
--- a/Sources/SQLiteNIO/SQLiteDataConvertible.swift
+++ b/Sources/SQLiteNIO/SQLiteDataConvertible.swift
@@ -1,4 +1,5 @@
import NIOCore
+import NIOFoundationCompat
import Foundation
public protocol SQLiteDataConvertible {
@@ -8,19 +9,20 @@ public protocol SQLiteDataConvertible {
extension String: SQLiteDataConvertible {
public init?(sqliteData: SQLiteData) {
- guard case .text(let value) = sqliteData else {
+ guard let value = sqliteData.string else {
return nil
}
self = value
}
public var sqliteData: SQLiteData? {
- return .text(self)
+ .text(self)
}
}
extension FixedWidthInteger {
public init?(sqliteData: SQLiteData) {
+ // Don't use `SQLiteData.integer`, we don't want to attempt converting strings here.
guard case .integer(let value) = sqliteData else {
return nil
}
@@ -28,7 +30,7 @@ extension FixedWidthInteger {
}
public var sqliteData: SQLiteData? {
- return .integer(numericCast(self))
+ .integer(numericCast(self))
}
}
@@ -45,27 +47,30 @@ extension UInt64: SQLiteDataConvertible { }
extension Double: SQLiteDataConvertible {
public init?(sqliteData: SQLiteData) {
- guard case .float(let value) = sqliteData else {
- return nil
+ // Don't use `SQLiteData.double`, we don't want to attempt converting strings here.
+ switch sqliteData {
+ case .integer(let int): self.init(int)
+ case .float(let double): self = double
+ case .text(_), .blob(_), .null: return nil
}
- self = value
}
public var sqliteData: SQLiteData? {
- return .float(self)
+ .float(self)
}
}
extension Float: SQLiteDataConvertible {
public init?(sqliteData: SQLiteData) {
- guard case .float(let value) = sqliteData else {
- return nil
+ switch sqliteData {
+ case .integer(let int): self.init(int)
+ case .float(let double): self.init(double)
+ case .text(_), .blob(_), .null: return nil
}
- self = Float(value)
}
public var sqliteData: SQLiteData? {
- return .float(Double(self))
+ .float(Double(self))
}
}
@@ -78,38 +83,33 @@ extension ByteBuffer: SQLiteDataConvertible {
}
public var sqliteData: SQLiteData? {
- return .blob(self)
+ .blob(self)
}
}
extension Data: SQLiteDataConvertible {
public init?(sqliteData: SQLiteData) {
- guard case .blob(var value) = sqliteData else {
- return nil
- }
- guard let data = value.readBytes(length: value.readableBytes) else {
+ guard case .blob(let value) = sqliteData else {
return nil
}
- self = Data(data)
+ self = .init(buffer: value, byteTransferStrategy: .copy)
}
public var sqliteData: SQLiteData? {
- var buffer = ByteBufferAllocator().buffer(capacity: self.count)
- buffer.writeBytes(self)
- return .blob(buffer)
+ .blob(.init(data: self))
}
}
extension Bool: SQLiteDataConvertible {
public init?(sqliteData: SQLiteData) {
guard let bool = sqliteData.bool else {
- return nil
- }
- self = bool
+ return nil
}
+ self = bool
+ }
public var sqliteData: SQLiteData? {
- return .integer(self ? 1 : 0)
+ .integer(self ? 1 : 0)
}
}
@@ -139,7 +139,7 @@ extension Date: SQLiteDataConvertible {
}
public var sqliteData: SQLiteData? {
- return .float(timeIntervalSince1970)
+ .float(timeIntervalSince1970)
}
}
diff --git a/Sources/SQLiteNIO/SQLiteDataType.swift b/Sources/SQLiteNIO/SQLiteDataType.swift
index f5bb47b..a899699 100644
--- a/Sources/SQLiteNIO/SQLiteDataType.swift
+++ b/Sources/SQLiteNIO/SQLiteDataType.swift
@@ -16,7 +16,6 @@ public enum SQLiteDataType {
/// `NULL`.
case null
- /// See `SQLSerializable`.
public func serialize(_ binds: inout [any Encodable]) -> String {
switch self {
case .integer: return "INTEGER"
diff --git a/Sources/SQLiteNIO/SQLiteDatabase.swift b/Sources/SQLiteNIO/SQLiteDatabase.swift
new file mode 100644
index 0000000..539a182
--- /dev/null
+++ b/Sources/SQLiteNIO/SQLiteDatabase.swift
@@ -0,0 +1,198 @@
+import NIOCore
+import NIOPosix
+import CSQLite
+import Logging
+
+/// A protocol describing the minimum requirements for an object allowing access to a generic SQLite database.
+///
+/// This protocol is intended to assist with connection pooling and other "smells like a simple database but isn't"
+/// use cases. In retrospect, it has become clear that it was poorly designed. Users and implementations alike
+/// should try to use ``SQLiteConnection`` directly whenever possible.
+public protocol SQLiteDatabase: Sendable {
+ /// The logger used by the connection.
+ var logger: Logger { get }
+
+ /// The event loop on which operations on the connection execute.
+ var eventLoop: any EventLoop { get }
+
+ /// Execute a query on the connection, calling the provided closure for each result row (if any).
+ ///
+ /// This is the primary interface to connections vended via this protocol.
+ ///
+ /// > Warning: The `logger` parameter of this method is a holdover from Fluent 4's development cycle that
+ /// > should have been removed before the final release. Unfortunately, this didn't happen, and semantic
+ /// > versioning has left the API stuck with it ever single. Callers of this API should either always pass
+ /// > the value of the ``logger`` property or use ``query(_:_:_:)`` instead. Implementations that wish to
+ /// > conform to this protocol should ignore the parameter entirely in favor of the ``logger`` property.
+ /// > At no time during SQLiteNIO's lifetime has this parameter ever been honored; indeed, at the time of
+ /// > this writing, ``SQLiteConnection``'s implementation of this method doesn't use _any_ logger at all.
+ ///
+ /// - Parameters:
+ /// - query: The query string to execute.
+ /// - binds: An ordered list of ``SQLiteData`` items to use as bound parameters for the query.
+ /// - logger: Ignored. See above discussion for details.
+ /// - onRow: A closure to invoke for each result row returned by the query, if any.
+ /// - Returns: A future completed when the query has executed and returned all results (if any).
+ @preconcurrency
+ func query(
+ _ query: String,
+ _ binds: [SQLiteData],
+ logger: Logger,
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) -> EventLoopFuture
+
+ /// Call the provided closure with a concrete ``SQLiteConnection`` instance.
+ ///
+ /// This method is required to provide a connection object which executes all queries directed to it in the
+ /// same "session" (e.g. always on the same connection, such as without rotating through a pool).
+ ///
+ /// - Parameter closure: The closure to invoke. Unless the closure changes the connection's state itself or the
+ /// connection is closed by SQLite due to error, it is guaranteed to remain valid until the future returned by
+ /// the closure is completed or failed.
+ /// - Returns: A future signaling completion of the closure and containing the closure's result, if any.
+ @preconcurrency
+ func withConnection(
+ _ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
+ ) -> EventLoopFuture
+}
+
+/// Convenience helpers and Concurrency-aware variants.
+extension SQLiteDatabase {
+ /// Convenience method for calling ``query(_:_:logger:_:)`` with the connection's logger.
+ ///
+ /// Callers are strongly encouraged to always use this method or its async equivalent (``query(_:_:_:)``) instead
+ /// of the protocol requirement.
+ @preconcurrency
+ public func query(
+ _ query: String,
+ _ binds: [SQLiteData] = [],
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) -> EventLoopFuture {
+ self.query(query, binds, logger: self.logger, onRow)
+ }
+
+ /// Convenience method for calling ``query(_:_:logger:_:)`` with the connection's logger (async version).
+ ///
+ /// Callers are strongly encouraged to always use this method or its futures-based equivalent (``query(_:_:_:)``)
+ /// instead of the protocol requirement.
+ public func query(
+ _ query: String,
+ _ binds: [SQLiteData] = [],
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) async throws {
+ try await self.query(query, binds, logger: self.logger, onRow).get()
+ }
+
+ /// Wrapper for ``query(_:_:_:)`` which returns the result rows (if any) rather than calling a closure.
+ public func query(_ query: String, _ binds: [SQLiteData] = []) -> EventLoopFuture<[SQLiteRow]> {
+ #if swift(<5.10)
+ let rows: UnsafeMutableTransferBox<[SQLiteRow]> = .init([])
+
+ return self.query(query, binds, logger: self.logger) { rows.wrappedValue.append($0) }.map { rows.wrappedValue }
+ #else
+ nonisolated(unsafe) var rows: [SQLiteRow] = []
+
+ return self.query(query, binds, logger: self.logger) { rows.append($0) }.map { rows }
+ #endif
+ }
+
+ /// Wrapper for ``query(_:_:_:)`` which returns the result rows (if any) rather than calling a
+ /// closure (async version).
+ public func query(_ query: String, _ binds: [SQLiteData] = []) async throws -> [SQLiteRow] {
+ try await self.query(query, binds).get()
+ }
+
+ /// Async version of ``withConnection(_:)-48y34``.
+ public func withConnection(
+ _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
+ ) async throws -> T {
+ try await self.withConnection { conn in
+ conn.eventLoop.makeFutureWithTask {
+ try await closure(conn)
+ }
+ }.get()
+ }
+}
+
+#if swift(<5.10)
+/// A wrapper type to avoid `Sendable` warnings for mutable captures that are otherwise safe.
+///
+/// This effectively acts as workaround for the absence of `nonisolated(unsafe)` before Swift 5.10.
+fileprivate final class UnsafeMutableTransferBox: @unchecked Sendable {
+ var wrappedValue: Wrapped
+ init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue }
+}
+#endif
+
+extension SQLiteDatabase {
+ /// Return a new ``SQLiteDatabase`` which is indistinguishable from the original save that its
+ /// ``SQLiteDatabase/logger`` property is replaced by the given `Logger`.
+ ///
+ /// This has the effect of redirecting logging performed on or by the original database to the
+ /// provided `Logger`.
+ ///
+ /// > Warning: The log redirection applies only to the new ``SQLiteDatabase`` that is returned from
+ /// > this method; logging operations performed on the original (i.e. `self`) are unaffected.
+ ///
+ /// > Note: Because this method returns a generic ``SQLiteDatabase``, the type it returns need not be public
+ /// > API. Unfortunately, this also means that no inlining or static dispatch of the implementation is
+ /// > possible, thus imposing a performance penalty on the use of this otherwise trivial utility.
+ ///
+ /// - Parameter logger: The new `Logger` to use.
+ /// - Returns: A database object which logs to the new `Logger`.
+ public func logging(to logger: Logger) -> any SQLiteDatabase {
+ SQLiteDatabaseCustomLogger(database: self, logger: logger)
+ }
+}
+
+/// Replaces the `Logger` of an existing ``SQLiteDatabase`` while forwarding all other properties and
+/// methods to the original.
+private struct SQLiteDatabaseCustomLogger: SQLiteDatabase {
+ /// The underlying database.
+ let database: D
+
+ // See `SQLiteDatabase.logger`.
+ let logger: Logger
+
+ // See `SQLiteDatabase.eventLoop`.
+ var eventLoop: any EventLoop { self.database.eventLoop }
+
+ // See `SQLiteDatabase.withConnection(_:)`.
+ func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture {
+ self.database.withConnection(closure)
+ }
+ // See `SQLiteDatabase.withConnection(_:)`.
+ func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T) async throws -> T {
+ try await self.database.withConnection(closure)
+ }
+
+ // See `SQLiteDatabase.query(_:_:_:)`.
+ func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
+ self.database.query(query, binds, logger: logger, onRow)
+ }
+
+ // See `SQLiteDatabase.query(_:_:_:)`.
+ func query(_ query: String, _ binds: [SQLiteData] = [], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
+ self.database.query(query, binds, onRow)
+ }
+ // See `SQLiteDatabase.query(_:_:_:)`.
+ func query(_ query: String, _ binds: [SQLiteData], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) async throws {
+ try await self.database.query(query, binds, onRow)
+ }
+
+ // See `SQLiteDatabase.query(_:_:)`.
+ func query(_ query: String, _ binds: [SQLiteData] = []) -> EventLoopFuture<[SQLiteRow]> {
+ self.database.query(query, binds)
+ }
+ // See `SQLiteDatabase.query(_:_:)`.
+ func query(_ query: String, _ binds: [SQLiteData] = []) async throws -> [SQLiteRow] {
+ try await self.database.query(query, binds)
+ }
+
+ // See `SQLiteDatabase.logger(_:)`.
+ func logging(to logger: Logger) -> any SQLiteDatabase {
+ /// N.B.: We explicitly override this method so that if ``SQLiteDatabase/logging(to:)`` is called in a nested
+ /// or chained fashion, methods still only have to be forwarded at most once.
+ Self(database: self.database, logger: logger)
+ }
+}
diff --git a/Sources/SQLiteNIO/SQLiteError.swift b/Sources/SQLiteNIO/SQLiteError.swift
index 611b74c..57deaa8 100644
--- a/Sources/SQLiteNIO/SQLiteError.swift
+++ b/Sources/SQLiteNIO/SQLiteError.swift
@@ -6,24 +6,25 @@ public struct SQLiteError: Error, CustomStringConvertible, LocalizedError {
public let message: String
public var description: String {
- return "\(self.reason): \(self.message)"
+ "\(self.reason): \(self.message)"
}
public var errorDescription: String? {
- return self.description
+ self.description
}
- internal init(reason: Reason, message: String) {
+ init(reason: Reason, message: String) {
self.reason = reason
self.message = message
}
- internal init(statusCode: Int32, connection: SQLiteConnection) {
+ init(statusCode: Int32, connection: SQLiteConnection) {
self.reason = .init(statusCode: statusCode)
self.message = connection.errorMessage ?? "Unknown"
}
- public enum Reason {
+ public enum Reason: Sendable {
+ // SQLite "basic" errors
case error
case intern
case permission
@@ -54,6 +55,33 @@ public struct SQLiteError: Error, CustomStringConvertible, LocalizedError {
case warning
case row
case done
+
+ // SQLite "extended" result codes
+ case errorMissingCollatingSequence, errorRetry, errorMissingSnapshot
+ case abortByRollback
+ case busyInRecovery, busyInSnapshot, busyTimeout
+ case lockedBySharedCache, lockedVirtualTable
+ case readonlyInRecovery, readonlyCantLock, readonlyInRollback, readonlyBackingMoved, readonlyDirectory
+ case ioErrorFailedRead, ioErrorIncompleteRead, ioErrorFailedWrite, ioErrorFailedSync, ioErrorFailedDirSync,
+ ioErrorFailedTruncate, ioErrorFailedStat, ioErrorFailedUnlock, ioErrorFailedReadLock,
+ ioErrorFailedDelete, ioErrorNoMemory, ioErrorFailedAccess, ioErrorFailedLockCheck,
+ ioErrorFailedAdvisoryLock, ioErrorFailedClose, ioErrorFailedSharedMemOpen, ioErrorFailedSharedMemSize,
+ ioErrorFailedSharedMemMap, ioErrorFailedDeleteNonexistent, ioErrorFailedMemoryMap, ioErrorCantFindTempdir,
+ ioErrorCygwinPath, ioErrorBadDataChecksum, ioErrorCorruptedFilesystem
+ case corruptVirtualTable, corruptSequenceSchema, corruptIndex
+ case cantOpenDirectory, cantOpenInvalidPath, cantOpenCygwinPath, cantOpenUnfollowedSymlink
+ case constraintCheckFailed, constraintCommitHookFailed, constraintForeignKeyFailed,
+ constraintUserFunctionFailed, constraintNotNullFailed, constraintPrimaryKeyFailed,
+ constraintTriggerFailed, constraintUniqueFailed, constraintVirtualTableFailed,
+ constraintUniqueRowIDFailed, constraintUpdateTriggerDeletedRow, constraintStrictDataTypeFailed
+ case authUnauthorizedUser
+ case noticeRecoverWAL, noticeRecoverRollback
+ case warningAutoindex
+
+
+ // The following five "reasons" are holdovers from early development; they have never used by the package
+ // are do not correspond to SQLite error codes. They should be considered deprecated, but are not marked
+ // as such as there would be no way to avoid the warning for users who switch over this enum.
case connection
case close
case prepare
@@ -62,138 +90,275 @@ public struct SQLiteError: Error, CustomStringConvertible, LocalizedError {
var statusCode: Int32 {
switch self {
- case .error:
- return SQLITE_ERROR
- case .intern:
- return SQLITE_INTERNAL
- case .abort:
- return SQLITE_ABORT
- case .permission:
- return SQLITE_PERM
- case .busy:
- return SQLITE_BUSY
- case .locked:
- return SQLITE_LOCKED
- case .noMemory:
- return SQLITE_NOMEM
- case .readOnly:
- return SQLITE_READONLY
- case .interrupt:
- return SQLITE_INTERRUPT
- case .ioError:
- return SQLITE_IOERR
- case .corrupt:
- return SQLITE_CORRUPT
- case .notFound:
- return SQLITE_NOTFOUND
- case .full:
- return SQLITE_FULL
- case .cantOpen:
- return SQLITE_CANTOPEN
- case .proto:
- return SQLITE_PROTOCOL
- case .empty:
- return SQLITE_EMPTY
- case .schema:
- return SQLITE_SCHEMA
- case .tooBig:
- return SQLITE_TOOBIG
- case .constraint:
- return SQLITE_CONSTRAINT
- case .mismatch:
- return SQLITE_MISMATCH
- case .misuse:
- return SQLITE_MISUSE
- case .noLFS:
- return SQLITE_NOLFS
- case .auth:
- return SQLITE_AUTH
- case .format:
- return SQLITE_FORMAT
- case .range:
- return SQLITE_RANGE
- case .notADatabase:
- return SQLITE_NOTADB
- case .notice:
- return SQLITE_NOTICE
- case .warning:
- return SQLITE_WARNING
- case .row:
- return SQLITE_ROW
- case .done:
- return SQLITE_DONE
- case .connection, .close, .prepare, .bind, .execute:
- return -1
+ case .error: return SQLITE_ERROR
+ case .intern: return SQLITE_INTERNAL
+ case .abort: return SQLITE_ABORT
+ case .permission: return SQLITE_PERM
+ case .busy: return SQLITE_BUSY
+ case .locked: return SQLITE_LOCKED
+ case .noMemory: return SQLITE_NOMEM
+ case .readOnly: return SQLITE_READONLY
+ case .interrupt: return SQLITE_INTERRUPT
+ case .ioError: return SQLITE_IOERR
+ case .corrupt: return SQLITE_CORRUPT
+ case .notFound: return SQLITE_NOTFOUND
+ case .full: return SQLITE_FULL
+ case .cantOpen: return SQLITE_CANTOPEN
+ case .proto: return SQLITE_PROTOCOL
+ case .empty: return SQLITE_EMPTY
+ case .schema: return SQLITE_SCHEMA
+ case .tooBig: return SQLITE_TOOBIG
+ case .constraint: return SQLITE_CONSTRAINT
+ case .mismatch: return SQLITE_MISMATCH
+ case .misuse: return SQLITE_MISUSE
+ case .noLFS: return SQLITE_NOLFS
+ case .auth: return SQLITE_AUTH
+ case .format: return SQLITE_FORMAT
+ case .range: return SQLITE_RANGE
+ case .notADatabase: return SQLITE_NOTADB
+ case .notice: return SQLITE_NOTICE
+ case .warning: return SQLITE_WARNING
+ case .row: return SQLITE_ROW
+ case .done: return SQLITE_DONE
+ case .errorMissingCollatingSequence: return SQLITE_ERROR_MISSING_COLLSEQ
+ case .errorRetry: return SQLITE_ERROR_RETRY
+ case .errorMissingSnapshot: return SQLITE_ERROR_SNAPSHOT
+ case .abortByRollback: return SQLITE_ABORT_ROLLBACK
+ case .busyInRecovery: return SQLITE_BUSY_RECOVERY
+ case .busyInSnapshot: return SQLITE_BUSY_SNAPSHOT
+ case .busyTimeout: return SQLITE_BUSY_TIMEOUT
+ case .lockedBySharedCache: return SQLITE_LOCKED_SHAREDCACHE
+ case .lockedVirtualTable: return SQLITE_LOCKED_VTAB
+ case .readonlyInRecovery: return SQLITE_READONLY_RECOVERY
+ case .readonlyCantLock: return SQLITE_READONLY_CANTLOCK
+ case .readonlyInRollback: return SQLITE_READONLY_ROLLBACK
+ case .readonlyBackingMoved: return SQLITE_READONLY_DBMOVED
+ case .readonlyDirectory: return SQLITE_READONLY_DIRECTORY
+ case .ioErrorFailedRead: return SQLITE_IOERR_READ
+ case .ioErrorIncompleteRead: return SQLITE_IOERR_SHORT_READ
+ case .ioErrorFailedWrite: return SQLITE_IOERR_WRITE
+ case .ioErrorFailedSync: return SQLITE_IOERR_FSYNC
+ case .ioErrorFailedDirSync: return SQLITE_IOERR_DIR_FSYNC
+ case .ioErrorFailedTruncate: return SQLITE_IOERR_TRUNCATE
+ case .ioErrorFailedStat: return SQLITE_IOERR_FSTAT
+ case .ioErrorFailedUnlock: return SQLITE_IOERR_UNLOCK
+ case .ioErrorFailedReadLock: return SQLITE_IOERR_RDLOCK
+ case .ioErrorFailedDelete: return SQLITE_IOERR_DELETE
+ case .ioErrorNoMemory: return SQLITE_IOERR_NOMEM
+ case .ioErrorFailedAccess: return SQLITE_IOERR_ACCESS
+ case .ioErrorFailedLockCheck: return SQLITE_IOERR_LOCK
+ case .ioErrorFailedAdvisoryLock: return SQLITE_IOERR_CHECKRESERVEDLOCK
+ case .ioErrorFailedClose: return SQLITE_IOERR_CLOSE
+ case .ioErrorFailedSharedMemOpen: return SQLITE_IOERR_SHMOPEN
+ case .ioErrorFailedSharedMemSize: return SQLITE_IOERR_SHMSIZE
+ case .ioErrorFailedSharedMemMap: return SQLITE_IOERR_SHMMAP
+ case .ioErrorFailedDeleteNonexistent: return SQLITE_IOERR_DELETE_NOENT
+ case .ioErrorFailedMemoryMap: return SQLITE_IOERR_MMAP
+ case .ioErrorCantFindTempdir: return SQLITE_IOERR_GETTEMPPATH
+ case .ioErrorCygwinPath: return SQLITE_IOERR_CONVPATH
+ case .ioErrorBadDataChecksum: return SQLITE_IOERR_DATA
+ case .ioErrorCorruptedFilesystem: return SQLITE_IOERR_CORRUPTFS
+ case .corruptVirtualTable: return SQLITE_CORRUPT_VTAB
+ case .corruptSequenceSchema: return SQLITE_CORRUPT_SEQUENCE
+ case .corruptIndex: return SQLITE_CORRUPT_INDEX
+ case .cantOpenDirectory: return SQLITE_CANTOPEN_ISDIR
+ case .cantOpenInvalidPath: return SQLITE_CANTOPEN_FULLPATH
+ case .cantOpenCygwinPath: return SQLITE_CANTOPEN_CONVPATH
+ case .cantOpenUnfollowedSymlink: return SQLITE_CANTOPEN_SYMLINK
+ case .constraintCheckFailed: return SQLITE_CONSTRAINT_CHECK
+ case .constraintCommitHookFailed: return SQLITE_CONSTRAINT_COMMITHOOK
+ case .constraintForeignKeyFailed: return SQLITE_CONSTRAINT_FOREIGNKEY
+ case .constraintUserFunctionFailed: return SQLITE_CONSTRAINT_FUNCTION
+ case .constraintNotNullFailed: return SQLITE_CONSTRAINT_NOTNULL
+ case .constraintPrimaryKeyFailed: return SQLITE_CONSTRAINT_PRIMARYKEY
+ case .constraintTriggerFailed: return SQLITE_CONSTRAINT_TRIGGER
+ case .constraintUniqueFailed: return SQLITE_CONSTRAINT_UNIQUE
+ case .constraintVirtualTableFailed: return SQLITE_CONSTRAINT_VTAB
+ case .constraintUniqueRowIDFailed: return SQLITE_CONSTRAINT_ROWID
+ case .constraintUpdateTriggerDeletedRow: return SQLITE_CONSTRAINT_PINNED
+ case .constraintStrictDataTypeFailed: return SQLITE_CONSTRAINT_DATATYPE
+ case .authUnauthorizedUser: return SQLITE_AUTH_USER
+ case .noticeRecoverWAL: return SQLITE_NOTICE_RECOVER_WAL
+ case .noticeRecoverRollback: return SQLITE_NOTICE_RECOVER_ROLLBACK
+ case .warningAutoindex: return SQLITE_WARNING_AUTOINDEX
+
+ case .connection, .close, .prepare, .bind, .execute: return -1
}
}
- internal init(statusCode: Int32) {
+ init(statusCode: Int32) {
switch statusCode {
- case SQLITE_ERROR:
- self = .error
- case SQLITE_INTERNAL:
- self = .intern
- case SQLITE_PERM:
- self = .permission
- case SQLITE_ABORT:
- self = .abort
- case SQLITE_BUSY:
- self = .busy
- case SQLITE_LOCKED:
- self = .locked
- case SQLITE_NOMEM:
- self = .noMemory
- case SQLITE_READONLY:
- self = .readOnly
- case SQLITE_INTERRUPT:
- self = .interrupt
- case SQLITE_IOERR:
- self = .ioError
- case SQLITE_CORRUPT:
- self = .corrupt
- case SQLITE_NOTFOUND:
- self = .notFound
- case SQLITE_FULL:
- self = .full
- case SQLITE_CANTOPEN:
- self = .cantOpen
- case SQLITE_PROTOCOL:
- self = .proto
- case SQLITE_EMPTY:
- self = .empty
- case SQLITE_SCHEMA:
- self = .schema
- case SQLITE_TOOBIG:
- self = .tooBig
- case SQLITE_CONSTRAINT:
- self = .constraint
- case SQLITE_MISMATCH:
- self = .mismatch
- case SQLITE_MISUSE:
- self = .misuse
- case SQLITE_NOLFS:
- self = .noLFS
- case SQLITE_AUTH:
- self = .auth
- case SQLITE_FORMAT:
- self = .format
- case SQLITE_RANGE:
- self = .range
- case SQLITE_NOTADB:
- self = .notADatabase
- case SQLITE_NOTICE:
- self = .notice
- case SQLITE_WARNING:
- self = .warning
- case SQLITE_ROW:
- self = .row
- case SQLITE_DONE:
- self = .done
- default:
- self = .error
+ case SQLITE_ERROR: self = .error
+ case SQLITE_INTERNAL: self = .intern
+ case SQLITE_PERM: self = .permission
+ case SQLITE_ABORT: self = .abort
+ case SQLITE_BUSY: self = .busy
+ case SQLITE_LOCKED: self = .locked
+ case SQLITE_NOMEM: self = .noMemory
+ case SQLITE_READONLY: self = .readOnly
+ case SQLITE_INTERRUPT: self = .interrupt
+ case SQLITE_IOERR: self = .ioError
+ case SQLITE_CORRUPT: self = .corrupt
+ case SQLITE_NOTFOUND: self = .notFound
+ case SQLITE_FULL: self = .full
+ case SQLITE_CANTOPEN: self = .cantOpen
+ case SQLITE_PROTOCOL: self = .proto
+ case SQLITE_EMPTY: self = .empty
+ case SQLITE_SCHEMA: self = .schema
+ case SQLITE_TOOBIG: self = .tooBig
+ case SQLITE_CONSTRAINT: self = .constraint
+ case SQLITE_MISMATCH: self = .mismatch
+ case SQLITE_MISUSE: self = .misuse
+ case SQLITE_NOLFS: self = .noLFS
+ case SQLITE_AUTH: self = .auth
+ case SQLITE_FORMAT: self = .format
+ case SQLITE_RANGE: self = .range
+ case SQLITE_NOTADB: self = .notADatabase
+ case SQLITE_NOTICE: self = .notice
+ case SQLITE_WARNING: self = .warning
+ case SQLITE_ROW: self = .row
+ case SQLITE_DONE: self = .done
+
+ case SQLITE_ERROR_MISSING_COLLSEQ: self = .errorMissingCollatingSequence
+ case SQLITE_ERROR_RETRY: self = .errorRetry
+ case SQLITE_ERROR_SNAPSHOT: self = .errorMissingSnapshot
+ case SQLITE_ABORT_ROLLBACK: self = .abortByRollback
+ case SQLITE_BUSY_RECOVERY: self = .busyInRecovery
+ case SQLITE_BUSY_SNAPSHOT: self = .busyInSnapshot
+ case SQLITE_BUSY_TIMEOUT: self = .busyTimeout
+ case SQLITE_LOCKED_SHAREDCACHE: self = .lockedBySharedCache
+ case SQLITE_LOCKED_VTAB: self = .lockedVirtualTable
+ case SQLITE_READONLY_RECOVERY: self = .readonlyInRecovery
+ case SQLITE_READONLY_CANTLOCK: self = .readonlyCantLock
+ case SQLITE_READONLY_ROLLBACK: self = .readonlyInRollback
+ case SQLITE_READONLY_DBMOVED: self = .readonlyBackingMoved
+ case SQLITE_READONLY_DIRECTORY: self = .readonlyDirectory
+ case SQLITE_IOERR_READ: self = .ioErrorFailedRead
+ case SQLITE_IOERR_SHORT_READ: self = .ioErrorIncompleteRead
+ case SQLITE_IOERR_WRITE: self = .ioErrorFailedWrite
+ case SQLITE_IOERR_FSYNC: self = .ioErrorFailedSync
+ case SQLITE_IOERR_DIR_FSYNC: self = .ioErrorFailedDirSync
+ case SQLITE_IOERR_TRUNCATE: self = .ioErrorFailedTruncate
+ case SQLITE_IOERR_FSTAT: self = .ioErrorFailedStat
+ case SQLITE_IOERR_UNLOCK: self = .ioErrorFailedUnlock
+ case SQLITE_IOERR_RDLOCK: self = .ioErrorFailedReadLock
+ case SQLITE_IOERR_DELETE: self = .ioErrorFailedDelete
+ case SQLITE_IOERR_NOMEM: self = .ioErrorNoMemory
+ case SQLITE_IOERR_ACCESS: self = .ioErrorFailedAccess
+ case SQLITE_IOERR_LOCK: self = .ioErrorFailedLockCheck
+ case SQLITE_IOERR_CHECKRESERVEDLOCK: self = .ioErrorFailedAdvisoryLock
+ case SQLITE_IOERR_CLOSE: self = .ioErrorFailedClose
+ case SQLITE_IOERR_SHMOPEN: self = .ioErrorFailedSharedMemOpen
+ case SQLITE_IOERR_SHMSIZE: self = .ioErrorFailedSharedMemSize
+ case SQLITE_IOERR_SHMMAP: self = .ioErrorFailedSharedMemMap
+ case SQLITE_IOERR_DELETE_NOENT: self = .ioErrorFailedDeleteNonexistent
+ case SQLITE_IOERR_MMAP: self = .ioErrorFailedMemoryMap
+ case SQLITE_IOERR_GETTEMPPATH: self = .ioErrorCantFindTempdir
+ case SQLITE_IOERR_CONVPATH: self = .ioErrorCygwinPath
+ case SQLITE_IOERR_DATA: self = .ioErrorBadDataChecksum
+ case SQLITE_IOERR_CORRUPTFS: self = .ioErrorCorruptedFilesystem
+ case SQLITE_CORRUPT_VTAB: self = .corruptVirtualTable
+ case SQLITE_CORRUPT_SEQUENCE: self = .corruptSequenceSchema
+ case SQLITE_CORRUPT_INDEX: self = .corruptIndex
+ case SQLITE_CANTOPEN_ISDIR: self = .cantOpenDirectory
+ case SQLITE_CANTOPEN_FULLPATH: self = .cantOpenInvalidPath
+ case SQLITE_CANTOPEN_CONVPATH: self = .cantOpenCygwinPath
+ case SQLITE_CANTOPEN_SYMLINK: self = .cantOpenUnfollowedSymlink
+ case SQLITE_CONSTRAINT_CHECK: self = .constraintCheckFailed
+ case SQLITE_CONSTRAINT_COMMITHOOK: self = .constraintCommitHookFailed
+ case SQLITE_CONSTRAINT_FOREIGNKEY: self = .constraintForeignKeyFailed
+ case SQLITE_CONSTRAINT_FUNCTION: self = .constraintUserFunctionFailed
+ case SQLITE_CONSTRAINT_NOTNULL: self = .constraintNotNullFailed
+ case SQLITE_CONSTRAINT_PRIMARYKEY: self = .constraintPrimaryKeyFailed
+ case SQLITE_CONSTRAINT_TRIGGER: self = .constraintTriggerFailed
+ case SQLITE_CONSTRAINT_UNIQUE: self = .constraintUniqueFailed
+ case SQLITE_CONSTRAINT_VTAB: self = .constraintVirtualTableFailed
+ case SQLITE_CONSTRAINT_ROWID: self = .constraintUniqueRowIDFailed
+ case SQLITE_CONSTRAINT_PINNED: self = .constraintUpdateTriggerDeletedRow
+ case SQLITE_CONSTRAINT_DATATYPE: self = .constraintStrictDataTypeFailed
+ case SQLITE_AUTH_USER: self = .authUnauthorizedUser
+ case SQLITE_NOTICE_RECOVER_WAL: self = .noticeRecoverWAL
+ case SQLITE_NOTICE_RECOVER_ROLLBACK: self = .noticeRecoverRollback
+ case SQLITE_WARNING_AUTOINDEX: self = .warningAutoindex
+
+ default: self = .error
}
}
}
}
-extension SQLiteError.Reason: Sendable {}
+/// Redefinitions of SQLite's extended result codes, from `sqlite3.h`. ClangImporter still doesn't import these.
+let SQLITE_ERROR_MISSING_COLLSEQ: Int32 = (SQLITE_ERROR | (1<<8))
+let SQLITE_ERROR_RETRY: Int32 = (SQLITE_ERROR | (2<<8))
+let SQLITE_ERROR_SNAPSHOT: Int32 = (SQLITE_ERROR | (3<<8))
+let SQLITE_IOERR_READ: Int32 = (SQLITE_IOERR | (1<<8))
+let SQLITE_IOERR_SHORT_READ: Int32 = (SQLITE_IOERR | (2<<8))
+let SQLITE_IOERR_WRITE: Int32 = (SQLITE_IOERR | (3<<8))
+let SQLITE_IOERR_FSYNC: Int32 = (SQLITE_IOERR | (4<<8))
+let SQLITE_IOERR_DIR_FSYNC: Int32 = (SQLITE_IOERR | (5<<8))
+let SQLITE_IOERR_TRUNCATE: Int32 = (SQLITE_IOERR | (6<<8))
+let SQLITE_IOERR_FSTAT: Int32 = (SQLITE_IOERR | (7<<8))
+let SQLITE_IOERR_UNLOCK: Int32 = (SQLITE_IOERR | (8<<8))
+let SQLITE_IOERR_RDLOCK: Int32 = (SQLITE_IOERR | (9<<8))
+let SQLITE_IOERR_DELETE: Int32 = (SQLITE_IOERR | (10<<8))
+let SQLITE_IOERR_BLOCKED: Int32 = (SQLITE_IOERR | (11<<8))
+let SQLITE_IOERR_NOMEM: Int32 = (SQLITE_IOERR | (12<<8))
+let SQLITE_IOERR_ACCESS: Int32 = (SQLITE_IOERR | (13<<8))
+let SQLITE_IOERR_CHECKRESERVEDLOCK: Int32 = (SQLITE_IOERR | (14<<8))
+let SQLITE_IOERR_LOCK: Int32 = (SQLITE_IOERR | (15<<8))
+let SQLITE_IOERR_CLOSE: Int32 = (SQLITE_IOERR | (16<<8))
+let SQLITE_IOERR_DIR_CLOSE: Int32 = (SQLITE_IOERR | (17<<8))
+let SQLITE_IOERR_SHMOPEN: Int32 = (SQLITE_IOERR | (18<<8))
+let SQLITE_IOERR_SHMSIZE: Int32 = (SQLITE_IOERR | (19<<8))
+let SQLITE_IOERR_SHMLOCK: Int32 = (SQLITE_IOERR | (20<<8))
+let SQLITE_IOERR_SHMMAP: Int32 = (SQLITE_IOERR | (21<<8))
+let SQLITE_IOERR_SEEK: Int32 = (SQLITE_IOERR | (22<<8))
+let SQLITE_IOERR_DELETE_NOENT: Int32 = (SQLITE_IOERR | (23<<8))
+let SQLITE_IOERR_MMAP: Int32 = (SQLITE_IOERR | (24<<8))
+let SQLITE_IOERR_GETTEMPPATH: Int32 = (SQLITE_IOERR | (25<<8))
+let SQLITE_IOERR_CONVPATH: Int32 = (SQLITE_IOERR | (26<<8))
+let SQLITE_IOERR_VNODE: Int32 = (SQLITE_IOERR | (27<<8))
+let SQLITE_IOERR_AUTH: Int32 = (SQLITE_IOERR | (28<<8))
+let SQLITE_IOERR_BEGIN_ATOMIC: Int32 = (SQLITE_IOERR | (29<<8))
+let SQLITE_IOERR_COMMIT_ATOMIC: Int32 = (SQLITE_IOERR | (30<<8))
+let SQLITE_IOERR_ROLLBACK_ATOMIC: Int32 = (SQLITE_IOERR | (31<<8))
+let SQLITE_IOERR_DATA: Int32 = (SQLITE_IOERR | (32<<8))
+let SQLITE_IOERR_CORRUPTFS: Int32 = (SQLITE_IOERR | (33<<8))
+let SQLITE_IOERR_IN_PAGE: Int32 = (SQLITE_IOERR | (34<<8))
+let SQLITE_LOCKED_SHAREDCACHE: Int32 = (SQLITE_LOCKED | (1<<8))
+let SQLITE_LOCKED_VTAB: Int32 = (SQLITE_LOCKED | (2<<8))
+let SQLITE_BUSY_RECOVERY: Int32 = (SQLITE_BUSY | (1<<8))
+let SQLITE_BUSY_SNAPSHOT: Int32 = (SQLITE_BUSY | (2<<8))
+let SQLITE_BUSY_TIMEOUT: Int32 = (SQLITE_BUSY | (3<<8))
+let SQLITE_CANTOPEN_NOTEMPDIR: Int32 = (SQLITE_CANTOPEN | (1<<8))
+let SQLITE_CANTOPEN_ISDIR: Int32 = (SQLITE_CANTOPEN | (2<<8))
+let SQLITE_CANTOPEN_FULLPATH: Int32 = (SQLITE_CANTOPEN | (3<<8))
+let SQLITE_CANTOPEN_CONVPATH: Int32 = (SQLITE_CANTOPEN | (4<<8))
+let SQLITE_CANTOPEN_SYMLINK: Int32 = (SQLITE_CANTOPEN | (6<<8))
+let SQLITE_CORRUPT_VTAB: Int32 = (SQLITE_CORRUPT | (1<<8))
+let SQLITE_CORRUPT_SEQUENCE: Int32 = (SQLITE_CORRUPT | (2<<8))
+let SQLITE_CORRUPT_INDEX: Int32 = (SQLITE_CORRUPT | (3<<8))
+let SQLITE_READONLY_RECOVERY: Int32 = (SQLITE_READONLY | (1<<8))
+let SQLITE_READONLY_CANTLOCK: Int32 = (SQLITE_READONLY | (2<<8))
+let SQLITE_READONLY_ROLLBACK: Int32 = (SQLITE_READONLY | (3<<8))
+let SQLITE_READONLY_DBMOVED: Int32 = (SQLITE_READONLY | (4<<8))
+let SQLITE_READONLY_CANTINIT: Int32 = (SQLITE_READONLY | (5<<8))
+let SQLITE_READONLY_DIRECTORY: Int32 = (SQLITE_READONLY | (6<<8))
+let SQLITE_ABORT_ROLLBACK: Int32 = (SQLITE_ABORT | (2<<8))
+let SQLITE_CONSTRAINT_CHECK: Int32 = (SQLITE_CONSTRAINT | (1<<8))
+let SQLITE_CONSTRAINT_COMMITHOOK: Int32 = (SQLITE_CONSTRAINT | (2<<8))
+let SQLITE_CONSTRAINT_FOREIGNKEY: Int32 = (SQLITE_CONSTRAINT | (3<<8))
+let SQLITE_CONSTRAINT_FUNCTION: Int32 = (SQLITE_CONSTRAINT | (4<<8))
+let SQLITE_CONSTRAINT_NOTNULL: Int32 = (SQLITE_CONSTRAINT | (5<<8))
+let SQLITE_CONSTRAINT_PRIMARYKEY: Int32 = (SQLITE_CONSTRAINT | (6<<8))
+let SQLITE_CONSTRAINT_TRIGGER: Int32 = (SQLITE_CONSTRAINT | (7<<8))
+let SQLITE_CONSTRAINT_UNIQUE: Int32 = (SQLITE_CONSTRAINT | (8<<8))
+let SQLITE_CONSTRAINT_VTAB: Int32 = (SQLITE_CONSTRAINT | (9<<8))
+let SQLITE_CONSTRAINT_ROWID: Int32 = (SQLITE_CONSTRAINT | (10<<8))
+let SQLITE_CONSTRAINT_PINNED: Int32 = (SQLITE_CONSTRAINT | (11<<8))
+let SQLITE_CONSTRAINT_DATATYPE: Int32 = (SQLITE_CONSTRAINT | (12<<8))
+let SQLITE_NOTICE_RECOVER_WAL: Int32 = (SQLITE_NOTICE | (1<<8))
+let SQLITE_NOTICE_RECOVER_ROLLBACK: Int32 = (SQLITE_NOTICE | (2<<8))
+let SQLITE_NOTICE_RBU: Int32 = (SQLITE_NOTICE | (3<<8))
+let SQLITE_WARNING_AUTOINDEX: Int32 = (SQLITE_WARNING | (1<<8))
+let SQLITE_AUTH_USER: Int32 = (SQLITE_AUTH | (1<<8))
diff --git a/Sources/SQLiteNIO/SQLiteRow.swift b/Sources/SQLiteNIO/SQLiteRow.swift
index f940368..abddcad 100644
--- a/Sources/SQLiteNIO/SQLiteRow.swift
+++ b/Sources/SQLiteNIO/SQLiteRow.swift
@@ -1,4 +1,4 @@
-public struct SQLiteColumn: CustomStringConvertible {
+public struct SQLiteColumn: CustomStringConvertible, Sendable {
public let name: String
public let data: SQLiteData
@@ -7,7 +7,7 @@ public struct SQLiteColumn: CustomStringConvertible {
}
}
-public struct SQLiteRow {
+public struct SQLiteRow: CustomStringConvertible, Sendable {
let columnOffsets: SQLiteColumnOffsets
let data: [SQLiteData]
@@ -23,23 +23,18 @@ public struct SQLiteRow {
}
return self.data[offset]
}
-}
-extension SQLiteRow: CustomStringConvertible {
public var description: String {
self.columns.description
}
}
-final class SQLiteColumnOffsets {
+struct SQLiteColumnOffsets: Sendable {
let offsets: [(String, Int)]
let lookupTable: [String: Int]
init(offsets: [(String, Int)]) {
self.offsets = offsets
- self.lookupTable = .init(offsets, uniquingKeysWith: { a, b in a })
+ self.lookupTable = .init(offsets, uniquingKeysWith: { a, _ in a })
}
}
-
-extension SQLiteRow: Sendable {}
-extension SQLiteColumnOffsets: Sendable {}
diff --git a/Sources/SQLiteNIO/SQLiteStatement.swift b/Sources/SQLiteNIO/SQLiteStatement.swift
index 5a7e470..e32c897 100644
--- a/Sources/SQLiteNIO/SQLiteStatement.swift
+++ b/Sources/SQLiteNIO/SQLiteStatement.swift
@@ -1,56 +1,62 @@
import NIOCore
import CSQLite
-internal struct SQLiteStatement {
+struct SQLiteStatement {
private var handle: OpaquePointer?
private let connection: SQLiteConnection
- internal init(query: String, on connection: SQLiteConnection) throws {
+ init(query: String, on connection: SQLiteConnection) throws {
self.connection = connection
- let ret = sqlite_nio_sqlite3_prepare_v2(connection.handle.raw, query, -1, &self.handle, nil)
+
+ let ret = sqlite_nio_sqlite3_prepare_v3(
+ connection.handle.raw,
+ query,
+ -1,
+ 0, // TODO: Look into figuring out when passing SQLITE_PREPARE_PERSISTENT would be apropos.
+ &self.handle,
+ nil
+ )
+ // Can't use self.check() here, there's nohting to finalize yet on failure.
guard ret == SQLITE_OK else {
throw SQLiteError(statusCode: ret, connection: connection)
}
}
-
- internal func bind(_ binds: [SQLiteData]) throws {
+
+ private mutating func check(_ ret: Int32) throws {
+ // We check it this way so that `SQLITE_DONE` causes a finalize without throwing an error.
+ if ret != SQLITE_OK, let handle = self.handle {
+ sqlite_nio_sqlite3_finalize(handle)
+ self.handle = nil
+ }
+
+ guard ret == SQLITE_OK || ret == SQLITE_DONE || ret == SQLITE_ROW else {
+ throw SQLiteError(statusCode: ret, connection: self.connection)
+ }
+ }
+
+ mutating func bind(_ binds: [SQLiteData]) throws {
for (i, bind) in binds.enumerated() {
- let i = Int32(i + 1)
+ let i = Int32(i + 1), ret: Int32
+
switch bind {
case .blob(let value):
- let count = Int32(value.readableBytes)
- let ret = value.withUnsafeReadableBytes { pointer in
- return sqlite_nio_sqlite3_bind_blob(self.handle, i, pointer.baseAddress, count, SQLITE_TRANSIENT)
- }
- guard ret == SQLITE_OK else {
- throw SQLiteError(statusCode: ret, connection: connection)
+ ret = value.withUnsafeReadableBytes {
+ sqlite_nio_sqlite3_bind_blob64(self.handle, i, $0.baseAddress, UInt64($0.count), SQLITE_TRANSIENT)
}
case .float(let value):
- let ret = sqlite_nio_sqlite3_bind_double(self.handle, i, value)
- guard ret == SQLITE_OK else {
- throw SQLiteError(statusCode: ret, connection: connection)
- }
+ ret = sqlite_nio_sqlite3_bind_double(self.handle, i, value)
case .integer(let value):
- let ret = sqlite_nio_sqlite3_bind_int64(self.handle, i, Int64(value))
- guard ret == SQLITE_OK else {
- throw SQLiteError(statusCode: ret, connection: connection)
- }
+ ret = sqlite_nio_sqlite3_bind_int64(self.handle, i, Int64(value))
case .null:
- let ret = sqlite_nio_sqlite3_bind_null(self.handle, i)
- if ret != SQLITE_OK {
- throw SQLiteError(statusCode: ret, connection: connection)
- }
+ ret = sqlite_nio_sqlite3_bind_null(self.handle, i)
case .text(let value):
- let strlen = Int32(value.utf8.count)
- let ret = sqlite_nio_sqlite3_bind_text(self.handle, i, value, strlen, SQLITE_TRANSIENT)
- guard ret == SQLITE_OK else {
- throw SQLiteError(statusCode: ret, connection: connection)
- }
+ ret = sqlite_nio_sqlite3_bind_text64(self.handle, i, value, UInt64(value.utf8.count), SQLITE_TRANSIENT, UInt8(SQLITE_UTF8))
}
+ try self.check(ret)
}
}
- internal func columns() throws -> SQLiteColumnOffsets {
+ mutating func columns() throws -> SQLiteColumnOffsets {
var columns: [(String, Int)] = []
let count = sqlite_nio_sqlite3_column_count(self.handle)
@@ -58,37 +64,27 @@ internal struct SQLiteStatement {
// iterate over column count and intialize columns once
// we will then re-use the columns for each row
- for i in 0.. SQLiteRow? {
- // step over the query, this will continue to return SQLITE_ROW
- // for as long as there are new rows to be fetched
- let step = sqlite_nio_sqlite3_step(self.handle)
- switch step {
- case SQLITE_DONE:
- // no results left
- let ret = sqlite_nio_sqlite3_finalize(self.handle)
- guard ret == SQLITE_OK else {
- throw SQLiteError(statusCode: ret, connection: connection)
- }
- return nil
+ mutating func nextRow(for columns: SQLiteColumnOffsets) throws -> SQLiteRow? {
+ /// Step over the query. This will continue to return `SQLITE_ROW` for as long as there are new rows to be fetched.
+ switch sqlite_nio_sqlite3_step(self.handle) {
case SQLITE_ROW:
+ // Row returned.
break
- default:
- throw SQLiteError(statusCode: step, connection: connection)
+ case let ret:
+ // No results left, or error.
+ // This check is explicitly guaranteed to finalize the statement if the code is SQLITE_DONE.
+ try self.check(ret)
+ return nil
}
- let count = sqlite_nio_sqlite3_column_count(self.handle)
- var row: [SQLiteData] = []
- for i in 0.. SQLiteData {
switch sqlite_nio_sqlite3_column_type(self.handle, offset) {
case SQLITE_INTEGER:
- let val = sqlite_nio_sqlite3_column_int64(self.handle, offset)
- let integer = Int(val)
- return .integer(integer)
+ return .integer(Int(sqlite_nio_sqlite3_column_int64(self.handle, offset)))
case SQLITE_FLOAT:
- let val = sqlite_nio_sqlite3_column_double(self.handle, offset)
- let double = Double(val)
- return .float(double)
+ return .float(Double(sqlite_nio_sqlite3_column_double(self.handle, offset)))
case SQLITE_TEXT:
guard let val = sqlite_nio_sqlite3_column_text(self.handle, offset) else {
throw SQLiteError(reason: .error, message: "Unexpected nil column text")
}
- let string = String(cString: val)
- return .text(string)
+ return .text(.init(cString: val))
case SQLITE_BLOB:
let length = Int(sqlite_nio_sqlite3_column_bytes(self.handle, offset))
var buffer = ByteBufferAllocator().buffer(capacity: length)
+
if let blobPointer = sqlite_nio_sqlite3_column_blob(self.handle, offset) {
- buffer.writeBytes(UnsafeBufferPointer(
- start: blobPointer.assumingMemoryBound(to: UInt8.self),
- count: length
- ))
+ buffer.writeBytes(UnsafeRawBufferPointer(start: blobPointer, count: length))
}
return .blob(buffer)
- case SQLITE_NULL: return .null
- default: throw SQLiteError(reason: .error, message: "Unexpected column type.")
+ case SQLITE_NULL:
+ return .null
+ default:
+ throw SQLiteError(reason: .error, message: "Unexpected column type")
}
}
private func column(at offset: Int32) throws -> String {
guard let cName = sqlite_nio_sqlite3_column_name(self.handle, offset) else {
- throw SQLiteError(reason: .error, message: "Unexpected nil column name")
+ throw SQLiteError(reason: .error, message: "Unexpectedly found a nil column name at offset \(offset)")
}
return String(cString: cName)
}
}
-internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
+let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
+
diff --git a/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift b/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift
index c63ce1b..0ca82bf 100644
--- a/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift
+++ b/Tests/SQLiteNIOTests/SQLiteCustomFunctionTests.swift
@@ -14,6 +14,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
*/
import XCTest
import SQLiteNIO
+import NIOFoundationCompat
private struct CustomValueType: SQLiteDataConvertible, Equatable {
init() {}
@@ -32,328 +33,236 @@ private struct CustomValueType: SQLiteDataConvertible, Equatable {
final class DatabaseFunctionTests: XCTestCase {
// MARK: - Return values
- func testFunctionReturningNull() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
+ func testFunctionReturningNull() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in nil }
+ try await conn.install(customFunction: fn)
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { dbValues in
- return nil
- }
- try conn.install(customFunction: fn).wait()
-
- XCTAssertTrue(try conn.query("SELECT f() as result").map { rows in rows[0].column("result")!.isNull }.wait())
+ await XCTAssertAsync(try await conn.query("SELECT f() as result").first?.column("result")?.isNull ?? false)
+ }
}
- func testFunctionReturningInt64() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { dbValues in
- return Int(1)
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertEqual(Int(1), try conn.query("SELECT f() as result").map { rows in rows[0].column("result")?.integer }.wait())
+ func testFunctionReturningInt64() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in 1 }
+ try await conn.install(customFunction: fn)
+ await XCTAssertEqualAsync(Int(1), try await conn.query("SELECT f() as result").first?.column("result")?.integer)
+ }
}
- func testFunctionReturningDouble() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { dbValues in
- return 1e100
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertEqual(1e100, try conn.query("SELECT f() as result").map { rows in rows[0].column("result")?.double }.wait())
+ func testFunctionReturningDouble() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in 1e100 }
+
+ try await conn.install(customFunction: fn)
+ await XCTAssertEqualAsync(1e100, try await conn.query("SELECT f() as result").first?.column("result")?.double)
+ }
}
- func testFunctionReturningString() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in
- return "foo"
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertEqual("foo", try conn.query("SELECT f() as result").map { rows in rows[0].column("result")?.string }.wait())
+ func testFunctionReturningString() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in "foo" }
+
+ try await conn.install(customFunction: fn)
+ await XCTAssertEqualAsync("foo", try await conn.query("SELECT f() as result").first?.column("result")?.string)
+ }
}
- func testFunctionReturningData() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in
- return "foo".data(using: .utf8)
- }
- try conn.install(customFunction: fn).wait()
-
- XCTAssertEqual("foo".data(using: .utf8)!.sqliteData!.blob!,
- try conn.query("SELECT f() as result").map { rows in rows[0].column("result")?.blob }.wait())
+ func testFunctionReturningData() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in Data("foo".utf8) }
+ try await conn.install(customFunction: fn)
- XCTAssertNotEqual("bar".data(using: .utf8)!.sqliteData!.blob!,
- try conn.query("SELECT f() as result").map { rows in rows[0].column("result")?.blob }.wait())
+ await XCTAssertEqualAsync(ByteBuffer(string: "foo"), try await conn.query("SELECT f() as result").first?.column("result")?.blob)
+ await XCTAssertNotEqualAsync(ByteBuffer(string: "bar"), try await conn.query("SELECT f() as result").first?.column("result")?.blob)
+ }
}
- func testFunctionReturningCustomValueType() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { dbValues in
- return CustomValueType()
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertEqual(CustomValueType().sqliteData, try conn.query("SELECT f() as result").map { rows in rows[0].column("result") }.wait())
+ func testFunctionReturningCustomValueType() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in CustomValueType() }
+
+ try await conn.install(customFunction: fn)
+ await XCTAssertEqualAsync(CustomValueType().sqliteData, try await conn.query("SELECT f() as result").first?.column("result"))
+ }
}
// MARK: - Argument values
- func testFunctionArgumentNil() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return values[0].isNull
- }
- try conn.install(customFunction: fn).wait()
-
- XCTAssertTrue(try conn.query("SELECT f(NULL) as result")
- .map { rows in rows[0].column("result")!.bool! }.wait())
- XCTAssertFalse(try conn.query("SELECT f(1) as result")
- .map { rows in rows[0].column("result")!.bool! }.wait())
- XCTAssertFalse(try conn.query("SELECT f(1.1) as result")
- .map { rows in rows[0].column("result")!.bool! }.wait())
- XCTAssertFalse(try conn.query("SELECT f('foo') as result")
- .map { rows in rows[0].column("result")!.bool! }.wait())
- XCTAssertFalse(try conn.query("SELECT f(?) as result", [.text("foo")])
- .map { rows in rows[0].column("result")!.bool! }.wait())
- }
+ func testFunctionArgumentNil() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in values[0].isNull }
+ try await conn.install(customFunction: fn)
- func testFunctionArgumentInt64() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return values[0].integer
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertNil(try conn.query("SELECT f(NULL) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
- XCTAssertEqual(1, try conn.query("SELECT f(1) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
- XCTAssertEqual(1, try conn.query("SELECT f(1.1) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
+ await XCTAssertTrueAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result")?.bool ?? false)
+ await XCTAssertFalseAsync(try await conn.query("SELECT f(1) as result").first?.column("result")?.bool ?? true)
+ await XCTAssertFalseAsync(try await conn.query("SELECT f(1.1) as result").first?.column("result")?.bool ?? true)
+ await XCTAssertFalseAsync(try await conn.query("SELECT f('foo') as result").first?.column("result")?.bool ?? true)
+ await XCTAssertFalseAsync(try await conn.query("SELECT f(?) as result", [.text("foo")]).first?.column("result")?.bool ?? true)
+ }
}
- func testFunctionArgumentDouble() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return values[0].double
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertNil(try conn.query("SELECT f(NULL) as result")
- .map { rows in rows[0].column("result")?.double }.wait())
- XCTAssertEqual(1.0, try conn.query("SELECT f(1) as result")
- .map { rows in rows[0].column("result")?.double }.wait())
- XCTAssertEqual(1.1, try conn.query("SELECT f(1.1) as result")
- .map { rows in rows[0].column("result")?.double }.wait())
+ func testFunctionArgumentInt64() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in values[0].integer }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertNilAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result")?.integer)
+ await XCTAssertEqualAsync(1, try await conn.query("SELECT f(1) as result").first?.column("result")?.integer)
+ await XCTAssertEqualAsync(1, try await conn.query("SELECT f(1.1) as result").first?.column("result")?.integer)
+ }
}
- func testFunctionArgumentString() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return values[0].string
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertNil(try conn.query("SELECT f(NULL) as result")
- .map { rows in rows[0].column("result")?.string }.wait())
- XCTAssertEqual("foo", try conn.query("SELECT f('foo') as result")
- .map { rows in rows[0].column("result")?.string }.wait())
+ func testFunctionArgumentDouble() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in values[0].double }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertNilAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result")?.double)
+ await XCTAssertEqualAsync(1.0, try await conn.query("SELECT f(1) as result").first?.column("result")?.double)
+ await XCTAssertEqualAsync(1.1, try await conn.query("SELECT f(1.1) as result").first?.column("result")?.double)
+ }
}
- func testFunctionArgumentBlob() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return values[0].blob
- }
- try conn.install(customFunction: fn).wait()
+ func testFunctionArgumentString() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in values[0].string }
+ try await conn.install(customFunction: fn)
- XCTAssertNil(try conn.query("SELECT f(NULL) as result")
- .map { rows in rows[0].column("result")?.blob }.wait())
+ await XCTAssertNilAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result")?.string)
+ await XCTAssertEqualAsync("foo", try await conn.query("SELECT f('foo') as result").first?.column("result")?.string)
+ }
+ }
- XCTAssertEqual("foo".data(using: .utf8)!.sqliteData!.blob, try conn.query("SELECT f(?) as result", ["foo".data(using: .utf8)!.sqliteData!])
- .map { rows in rows[0].column("result")?.blob }.wait())
+ func testFunctionArgumentBlob() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in values[0].blob }
+ try await conn.install(customFunction: fn)
- XCTAssertEqual(ByteBuffer(), try conn.query("SELECT f(?) as result", [.blob(ByteBuffer())])
- .map { rows in rows[0].column("result")?.blob }.wait())
+ await XCTAssertNilAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result")?.blob)
+ await XCTAssertEqualAsync(ByteBuffer(string: "foo"), try await conn.query("SELECT f(?) as result", [.blob(ByteBuffer(string: "foo"))]).first?.column("result")?.blob)
+ await XCTAssertEqualAsync(ByteBuffer(), try await conn.query("SELECT f(?) as result", [.blob(ByteBuffer())]).first?.column("result")?.blob)
+ }
}
- func testFunctionArgumentCustomValueType() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return CustomValueType(sqliteData: values[0])
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertNil(try conn.query("SELECT f(NULL) as result")
- .map { rows in CustomValueType(sqliteData: rows[0].column("result")!) }.wait())
- XCTAssertEqual(CustomValueType(), try conn.query("SELECT f('CustomValueType') as result")
- .map { rows in CustomValueType(sqliteData: rows[0].column("result")!) }.wait())
+ func testFunctionArgumentCustomValueType() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in CustomValueType(sqliteData: values[0]) }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertNilAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result").flatMap(CustomValueType.init(sqliteData:)))
+ await XCTAssertEqualAsync(CustomValueType(), try await conn.query("SELECT f('CustomValueType') as result").first?.column("result").flatMap(CustomValueType.init(sqliteData:)))
+ }
}
// MARK: - Argument count
- func testFunctionWithoutArgument() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { (values: [SQLiteData]) in
- return "foo"
- }
- try conn.install(customFunction: fn).wait()
- XCTAssertEqual("foo", try conn.query("SELECT f() as result")
- .map { rows in rows[0].column("result")?.string }.wait())
-
- do {
- _ = try conn.query("SELECT f(1)").wait()
- } catch let error as SQLiteError {
- XCTAssertEqual(error.reason, .error)
- XCTAssertEqual(error.message, "wrong number of arguments to function f()")
- }
- }
-
- func testFunctionOfOneArgument() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f", argumentCount: 1) { (values: [SQLiteData]) in
- return values.first?.string?.uppercased()
- }
-
- try conn.install(customFunction: fn).wait()
-
- XCTAssertNil(try conn.query("SELECT f(NULL) as result")
- .map { rows in rows[0].column("result")?.string }.wait())
- XCTAssertEqual("ROUé", try conn.query("SELECT upper(?) as result", [.text("Roué")])
- .map { rows in rows[0].column("result")?.string }.wait())
- XCTAssertEqual("ROUÉ", try conn.query("SELECT f(?) as result", [.text("Roué")])
- .map { rows in rows[0].column("result")?.string }.wait())
-
- do {
- _ = try conn.query("SELECT f()").wait()
- } catch let error as SQLiteError {
- XCTAssertEqual(error.reason, .error)
- XCTAssertEqual(error.message, "wrong number of arguments to function f()")
- }
+ func testFunctionWithoutArgument() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in "foo" }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertEqualAsync("foo", try await conn.query("SELECT f() as result").first?.column("result")?.string)
+ await XCTAssertThrowsErrorAsync(try await conn.query("SELECT f(1)")) {
+ guard let error = $0 as? SQLiteError else { return XCTFail("Expected SQLiteError, got \(String(reflecting: $0))") }
+ XCTAssertEqual(error.reason, .error)
+ XCTAssertEqual(error.message, "wrong number of arguments to function f()")
+ }
+ }
}
- func testFunctionOfTwoArguments() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let fn = SQLiteCustomFunction("f", argumentCount: 2) { (values: [SQLiteData]) in
- values
- .compactMap { $0.integer }
- .reduce(0, +)
- }
-
- try conn.install(customFunction: fn).wait()
- XCTAssertEqual(3, try conn.query("SELECT f(1, 2) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
+ func testFunctionOfOneArgument() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 1) { values in values.first?.string?.uppercased() }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertNilAsync(try await conn.query("SELECT f(NULL) as result").first?.column("result")?.string)
+ await XCTAssertEqualAsync("ROUé", try await conn.query("SELECT upper(?) as result", [.text("Roué")]).first?.column("result")?.string)
+ await XCTAssertEqualAsync("ROUÉ", try await conn.query("SELECT f(?) as result", [.text("Roué")]).first?.column("result")?.string)
+ await XCTAssertThrowsErrorAsync(try await conn.query("SELECT f()")) {
+ guard let error = $0 as? SQLiteError else { return XCTFail("Expected SQLiteError, got \(String(reflecting: $0))") }
+ XCTAssertEqual(error.reason, .error)
+ XCTAssertEqual(error.message, "wrong number of arguments to function f()")
+ }
+ }
+ }
- do {
- _ = try conn.query("SELECT f()").wait()
- } catch let error as SQLiteError {
- XCTAssertEqual(error.reason, .error)
- XCTAssertEqual(error.message, "wrong number of arguments to function f()")
- }
+ func testFunctionOfTwoArguments() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f", argumentCount: 2) { values in values.compactMap { $0.integer }.reduce(0, +) }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertEqualAsync(3, try await conn.query("SELECT f(1, 2) as result").first?.column("result")?.integer)
+ await XCTAssertThrowsErrorAsync(try await conn.query("SELECT f()")) {
+ guard let error = $0 as? SQLiteError else { return XCTFail("Expected SQLiteError, got \(String(reflecting: $0))") }
+ XCTAssertEqual(error.reason, .error)
+ XCTAssertEqual(error.message, "wrong number of arguments to function f()")
+ }
+ }
}
- func testVariadicFunction() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
+ func testVariadicFunction() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f") { values in values.count }
+ try await conn.install(customFunction: fn)
- let fn = SQLiteCustomFunction("f") { (values: [SQLiteData]) in
- values.count
- }
- try conn.install(customFunction: fn).wait()
-
- XCTAssertEqual(0, try conn.query("SELECT f() as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
- XCTAssertEqual(1, try conn.query("SELECT f(1) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
- XCTAssertEqual(2, try conn.query("SELECT f(1, 2) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
- XCTAssertEqual(3, try conn.query("SELECT f(1, 1, 1) as result")
- .map { rows in rows[0].column("result")?.integer }.wait())
+ await XCTAssertEqualAsync(0, try await conn.query("SELECT f() as result").first?.column("result")?.integer)
+ await XCTAssertEqualAsync(1, try await conn.query("SELECT f(1) as result").first?.column("result")?.integer)
+ await XCTAssertEqualAsync(2, try await conn.query("SELECT f(1, 2) as result").first?.column("result")?.integer)
+ await XCTAssertEqualAsync(3, try await conn.query("SELECT f(1, 1, 1) as result").first?.column("result")?.integer)
+ }
}
// MARK: - Errors
- func testFunctionThrowingDatabaseCustomErrorWithMessage() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- struct MyError: Error {
- let message: String
- }
-
- let fn = SQLiteCustomFunction("f") { _ in
- throw MyError(message: "custom message")
- }
-
- try conn.install(customFunction: fn).wait()
-
- do {
- _ = try conn.query("SELECT f()").wait()
- XCTFail("Expected Error")
- } catch let error as MyError {
- XCTFail("expected this not to match")
- XCTAssertEqual(error.message, "custom message")
- } catch let error as SQLiteError {
-
- XCTAssertEqual(error.reason, .error)
- XCTAssertEqual(error.message, "MyError(message: \"custom message\")")
- }
+ func testFunctionThrowingDatabaseCustomErrorWithMessage() async throws {
+ try await withOpenedConnection { conn in
+ struct MyError: Error { let message: String }
+ let fn = SQLiteCustomFunction("f") { _ in throw MyError(message: "custom message") }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertThrowsErrorAsync(try await conn.query("SELECT f()")) {
+ guard let error = $0 as? SQLiteError else { return XCTFail("Expected SQLiteError, got \(String(reflecting: $0))") }
+ XCTAssertEqual(error.reason, .error)
+ XCTAssertEqual(error.message, "MyError(message: \"custom message\")")
+ }
+ }
}
- func testFunctionThrowingNSError() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
- let fn = SQLiteCustomFunction("f") { _ in
- throw NSError(domain: "CustomErrorDomain", code: 123, userInfo: [NSLocalizedDescriptionKey: "custom error message", NSLocalizedFailureReasonErrorKey: "custom error message"])
- }
-
- try conn.install(customFunction: fn).wait()
-
- do {
- _ = try conn.query("SELECT f()").wait()
- XCTFail("Expected Error")
- } catch let error as SQLiteError {
- XCTAssertEqual(error.reason, .error)
- XCTAssertTrue(error.message.contains("CustomErrorDomain"))
- XCTAssertTrue(error.message.contains("123"))
- XCTAssertTrue(error.message.contains("custom error message"), "expected '\(error.message)' to contain 'custom error message'")
- }
+ func testFunctionThrowingNSError() async throws {
+ try await withOpenedConnection { conn in
+ let fn = SQLiteCustomFunction("f") { _ in
+ throw NSError(domain: "CustomErrorDomain", code: 123, userInfo: [NSLocalizedDescriptionKey: "custom error message", NSLocalizedFailureReasonErrorKey: "custom error message"])
+ }
+ try await conn.install(customFunction: fn)
+
+ await XCTAssertThrowsErrorAsync(try await conn.query("SELECT f()")) {
+ guard let error = $0 as? SQLiteError else { return XCTFail("Expected SQLiteError, got \(String(reflecting: $0))") }
+ XCTAssertEqual(error.reason, .error)
+ XCTAssertTrue(error.message.contains("CustomErrorDomain"))
+ XCTAssertTrue(error.message.contains("123"))
+ XCTAssertTrue(error.message.contains("custom error message"), "expected '\(error.message)' to contain 'custom error message'")
+ }
+ }
}
// MARK: - Misc
- func testFunctionsAreClosures() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- final class QuickBox: @unchecked Sendable {
- var value: T
- init(_ value: T) { self.value = value }
+ func testFunctionsCanBeExtremelyUnsafeClosures() async throws {
+ try await withOpenedConnection { conn in
+ final class QuickBox: @unchecked Sendable { var value: T; init(_ value: T) { self.value = value } }
+ let x = QuickBox(123)
+ let fn = SQLiteCustomFunction("f", argumentCount: 0) { values in x.value }
+ try await conn.install(customFunction: fn)
+
+ x.value = 321
+ await XCTAssertEqualAsync(321, try await conn.query("SELECT f() as result").first?.column("result")?.integer)
}
- let x = QuickBox(123)
- let fn = SQLiteCustomFunction("f", argumentCount: 0) { dbValues in
- x.value
- }
- try conn.install(customFunction: fn).wait()
- x.value = 321
- XCTAssertEqual(321, try conn.query("SELECT f() as result").map({ rows in rows[0].column("result")?.integer }).wait())
}
// MARK: - setup
- var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() }
-
- override func setUpWithError() throws {
+ override class func setUp() {
XCTAssert(isLoggingConfigured)
}
}
diff --git a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift
index 2b0fa9d..bfe2162 100644
--- a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift
+++ b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift
@@ -3,76 +3,92 @@ 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
+/// pool and event loop, guaranteeing that the connection is correctly cleaned up afterwards regardless of errors.
+func withOpenedConnection(
+ _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
+) async throws -> T {
+ let connection = try await SQLiteConnection.open(storage: .memory)
+
+ do {
+ let result = try await closure(connection)
+ try await connection.close()
+
+ return result
+ } catch {
+ try? await connection.close()
+ throw error
+ }
+
+}
final class SQLiteNIOTests: XCTestCase {
- func testBasicConnection() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
+ func testBasicConnection() async throws {
+ try await withOpenedConnection { conn in
+ let rows = try await conn.query("SELECT sqlite_version()")
- let rows = try conn.query("SELECT sqlite_version()").wait()
- XCTAssertEqual(rows.count, 1)
- XCTAssertNoThrow(try conn.query("PRAGMA compile_options").wait())
+ XCTAssertEqual(rows.count, 1)
+ await XCTAssertNoThrowAsync(try await conn.query("PRAGMA compile_options"))
+ }
}
- func testConnectionClosedThreadPool() throws {
+ func testConnectionClosedThreadPool() async throws {
let threadPool = NIOThreadPool(numberOfThreads: 1)
- try threadPool.syncShutdownGracefully()
+ try await threadPool.shutdownGracefully()
+
// This should error, but not create a leaking promise fatal error
- XCTAssertThrowsError(try SQLiteConnection.open(storage: .memory, threadPool: threadPool, on: self.eventLoop).wait())
+ await XCTAssertThrowsErrorAsync(try await SQLiteConnection.open(storage: .memory, threadPool: threadPool, on: MultiThreadedEventLoopGroup.singleton.any()))
}
- func testZeroLengthBlob() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let rows = try conn.query("SELECT zeroblob(0) as zblob").wait()
+ func testZeroLengthBlob() async throws {
+ try await withOpenedConnection { conn in
+ let rows = try await conn.query("SELECT zeroblob(0) as zblob")
- XCTAssertEqual(rows.count, 1)
+ XCTAssertEqual(rows.count, 1)
+ }
}
- func testDateFormat() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- XCTAssertEqual(Date(sqliteData: .text("2023-03-10"))?.timeIntervalSince1970, 1678406400)
-
- let rows = try conn.query("SELECT CURRENT_DATE").wait()
- XCTAssertNotNil(Date(sqliteData: rows[0].column("CURRENT_DATE")!))
+ func testDateFormat() async throws {
+ try await withOpenedConnection { conn in
+ XCTAssertEqual(Date(sqliteData: .text("2023-03-10"))?.timeIntervalSince1970, 1678406400)
+
+ let rows = try await conn.query("SELECT CURRENT_DATE")
+ XCTAssertNotNil(rows.first?.column("CURRENT_DATE").flatMap(Date.init(sqliteData:)))
+ }
}
- func testDateTimeFormat() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- XCTAssertEqual(Date(sqliteData: .text("2023-03-10 23:54:27"))?.timeIntervalSince1970, 1678492467)
-
- let rows = try conn.query("SELECT CURRENT_TIMESTAMP").wait()
- XCTAssertNotNil(Date(sqliteData: rows[0].column("CURRENT_TIMESTAMP")!))
+ func testDateTimeFormat() async throws {
+ try await withOpenedConnection { conn in
+ XCTAssertEqual(Date(sqliteData: .text("2023-03-10 23:54:27"))?.timeIntervalSince1970, 1678492467)
+
+ let rows = try await conn.query("SELECT CURRENT_TIMESTAMP")
+ XCTAssertNotNil(rows.first?.column("CURRENT_TIMESTAMP").flatMap(Date.init(sqliteData:)))
+ }
}
- func testTimestampStorage() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let date = Date()
- let rows = try conn.query("SELECT ? as date", [date.sqliteData!]).wait()
- XCTAssertEqual(rows[0].column("date"), .float(date.timeIntervalSince1970))
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!)?.description, date.description)
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!), date)
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!)?.timeIntervalSinceReferenceDate, date.timeIntervalSinceReferenceDate)
+ func testTimestampStorage() async throws {
+ try await withOpenedConnection { conn in
+ let date = Date()
+ let rows = try await conn.query("SELECT ? as date", [date.sqliteData!])
+ XCTAssertEqual(rows.first?.column("date"), .float(date.timeIntervalSince1970))
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.description, date.description)
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:)), date)
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.timeIntervalSinceReferenceDate, date.timeIntervalSinceReferenceDate)
+ }
}
- func testTimestampStorageRoundToMicroseconds() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- // Test value that when read back out of sqlite results in 7 decimal places that we need to round to microseconds
- let date = Date(timeIntervalSinceReferenceDate: 689658914.293192)
- let rows = try conn.query("SELECT ? as date", [date.sqliteData!]).wait()
- XCTAssertEqual(rows[0].column("date"), .float(date.timeIntervalSince1970))
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!)?.description, date.description)
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!), date)
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!)?.timeIntervalSinceReferenceDate, date.timeIntervalSinceReferenceDate)
+ func testTimestampStorageRoundToMicroseconds() async throws {
+ try await withOpenedConnection { conn in
+ // Test value that when read back out of sqlite results in 7 decimal places that we need to round to microseconds
+ let date = Date(timeIntervalSinceReferenceDate: 689658914.293192)
+ let rows = try await conn.query("SELECT ? as date", [date.sqliteData!])
+ XCTAssertEqual(rows.first?.column("date"), .float(date.timeIntervalSince1970))
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.description, date.description)
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:)), date)
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.timeIntervalSinceReferenceDate, date.timeIntervalSinceReferenceDate)
+ }
}
func testDateRoundToMicroseconds() throws {
@@ -85,108 +101,113 @@ final class SQLiteNIOTests: XCTestCase {
XCTAssertEqual(date.sqliteData, .float(secondsSinceUnixEpoch))
}
- func testTimestampStorageInDateColumnIntegralValue() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let date = Date(timeIntervalSince1970: 42)
- // This is how a column of type .date is crated when using Vapor’s
- // scheme table creation.
- _ = try conn.query(#"CREATE TABLE "test" ("date" DATE NOT NULL);"#).wait()
- _ = try conn.query(#"INSERT INTO test (date) VALUES (?);"#, [date.sqliteData!]).wait()
- let rows = try conn.query("SELECT * FROM test;").wait()
- XCTAssertTrue(rows[0].column("date") == .float(date.timeIntervalSince1970) || rows[0].column("date") == .integer(Int(date.timeIntervalSince1970)))
- XCTAssertEqual(Date(sqliteData: rows[0].column("date")!)?.description, date.description)
+ func testTimestampStorageInDateColumnIntegralValue() async throws {
+ try await withOpenedConnection { conn in
+ let date = Date(timeIntervalSince1970: 42)
+ // This is how a column of type .date is crated when using Vapor’s
+ // scheme table creation.
+ _ = try await conn.query(#"CREATE TABLE "test" ("date" DATE NOT NULL)"#)
+ _ = try await conn.query(#"INSERT INTO test (date) VALUES (?)"#, [date.sqliteData!])
+ let rows = try await conn.query("SELECT * FROM test")
+
+ XCTAssertTrue(rows.first?.column("date") == .float(date.timeIntervalSince1970) || rows.first?.column("date") == .integer(Int(date.timeIntervalSince1970)))
+ XCTAssertEqual(rows.first?.column("date").flatMap(Date.init(sqliteData:))?.description, date.description)
+ }
}
- func testDuplicateColumnName() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let rows = try conn.query("SELECT 1 as foo, 2 as foo").wait()
- var i = 0
- for column in rows[0].columns {
- XCTAssertEqual(column.name, "foo")
- i += column.data.integer!
+ func testDuplicateColumnName() async throws {
+ try await withOpenedConnection { conn in
+ let rows = try await conn.query("SELECT 1 as foo, 2 as foo")
+ let row0 = try XCTUnwrap(rows.first)
+ var i = 0
+ for column in row0.columns {
+ XCTAssertEqual(column.name, "foo")
+ i += column.data.integer ?? 0
+ }
+ XCTAssertEqual(i, 3)
+ XCTAssertEqual(row0.column("foo")?.integer, 1)
+ XCTAssertEqual(row0.columns.filter { $0.name == "foo" }.dropFirst(0).first?.data.integer, 1)
+ XCTAssertEqual(row0.columns.filter { $0.name == "foo" }.dropFirst(1).first?.data.integer, 2)
}
- XCTAssertEqual(i, 3)
- XCTAssertEqual(rows[0].column("foo")?.integer, 1)
- XCTAssertEqual(rows[0].columns.filter { $0.name == "foo" }[0].data.integer, 1)
- XCTAssertEqual(rows[0].columns.filter { $0.name == "foo" }[1].data.integer, 2)
}
- func testCustomAggregate() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
+ func testCustomAggregate() async throws {
+ try await withOpenedConnection { conn in
+ _ = try await conn.query(#"CREATE TABLE "scores" ("score" INTEGER NOT NULL)"#)
+ _ = try await conn.query(#"INSERT INTO scores (score) VALUES (?), (?), (?)"#, [.integer(3), .integer(4), .integer(5)])
- _ = try conn.query(#"CREATE TABLE "scores" ("score" INTEGER NOT NULL);"#).wait()
- _ = try conn.query(#"INSERT INTO scores (score) VALUES (?), (?), (?);"#, [.integer(3), .integer(4), .integer(5)]).wait()
+ struct MyAggregate: SQLiteCustomAggregate {
+ var sum: Int = 0
+ mutating func step(_ values: [SQLiteData]) throws {
+ self.sum += (values.first?.integer ?? 0)
+ }
- struct MyAggregate: SQLiteCustomAggregate {
- var sum: Int = 0
- mutating func step(_ values: [SQLiteData]) throws {
- sum = sum + (values.first?.integer ?? 0)
- }
+ func finalize() throws -> (any SQLiteDataConvertible)? {
+ self.sum
+ }
+ }
- func finalize() throws -> (any SQLiteDataConvertible)? {
- sum
- }
- }
+ let function = SQLiteCustomFunction("my_sum", argumentCount: 1, pure: true, aggregate: MyAggregate.self)
+ try await conn.install(customFunction: function)
- let function = SQLiteCustomFunction("my_sum", argumentCount: 1, pure: true, aggregate: MyAggregate.self)
- _ = try conn.install(customFunction: function).wait()
-
- let rows = try conn.query("SELECT my_sum(score) as total_score FROM scores").wait()
- XCTAssertEqual(rows.first?.column("total_score")?.integer, 12)
+ let rows = try await conn.query("SELECT my_sum(score) as total_score FROM scores")
+ XCTAssertEqual(rows.first?.column("total_score")?.integer, 12)
+ }
}
- func testDatabaseFunction() throws {
- let conn = try SQLiteConnection.open(storage: .memory, threadPool: .singleton, on: self.eventLoop).wait()
- defer { try! conn.close().wait() }
-
- let function = SQLiteCustomFunction("my_custom_function", argumentCount: 1, pure: true) { args in
- return Int(args[0].integer! * 3)
- }
+ func testDatabaseFunction() async throws {
+ try await withOpenedConnection { conn in
+ let function = SQLiteCustomFunction("my_custom_function", argumentCount: 1, pure: true) { args in
+ Int(args[0].integer! * 3)
+ }
- _ = try conn.install(customFunction: function).wait()
- let rows = try conn.query("SELECT my_custom_function(2) as my_value").wait()
- XCTAssertEqual(rows.first?.column("my_value")?.integer, 6)
+ _ = try await conn.install(customFunction: function)
+ let rows = try await conn.query("SELECT my_custom_function(2) as my_value")
+ XCTAssertEqual(rows.first?.column("my_value")?.integer, 6)
+ }
}
func testSingletonEventLoopOpen() async throws {
- var conn: SQLiteConnection! = nil
+ var conn: SQLiteConnection? = nil
await XCTAssertNoThrowAsync(conn = try await SQLiteConnection.open(storage: .memory).get())
- try await conn.close().get()
+ try await conn?.close().get()
+ }
+
+ func testSerializedConnectionAccess() async throws {
+ /// Although this test has no assertions, it does serve a useful purpose: when run with Thread Sanitizer
+ /// enabed, it validates that we are using SQLite in "serialized" mode (e.g. it is safe to use a single
+ /// connection simultaneously from multiple threads) rather than single- or multi-threaded mode.
+ try await withOpenedConnection { conn in
+ let t1 = Task {
+ for _ in 0 ..< 100 {
+ _ = try await conn.query("SELECT random()", [], { _ in })
+ }
+ }
+ let t2 = Task {
+ for _ in 0 ..< 100 {
+ _ = try await conn.query("SELECT random()", [], { _ in })
+ }
+ }
+
+ try await t1.value
+ try await t2.value
+ }
}
- var eventLoop: any EventLoop { MultiThreadedEventLoopGroup.singleton.any() }
-
- override func setUpWithError() throws {
+ override class func setUp() {
XCTAssert(isLoggingConfigured)
}
}
+func env(_ name: String) -> String? {
+ ProcessInfo.processInfo.environment[name]
+}
+
let isLoggingConfigured: Bool = {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
- handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .info
+ handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .info
return handler
}
return true
}()
-
-func env(_ name: String) -> String? {
- ProcessInfo.processInfo.environment[name]
-}
-
-func XCTAssertNoThrowAsync(
- _ expression: @autoclosure () async throws -> T,
- _ message: @autoclosure () -> String = "",
- file: StaticString = #filePath, line: UInt = #line
-) async {
- do {
- _ = try await expression()
- } catch {
- XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line)
- }
-}
diff --git a/Tests/SQLiteNIOTests/XCTAsyncAssertions.swift b/Tests/SQLiteNIOTests/XCTAsyncAssertions.swift
new file mode 100644
index 0000000..1f8b6e1
--- /dev/null
+++ b/Tests/SQLiteNIOTests/XCTAsyncAssertions.swift
@@ -0,0 +1,266 @@
+import XCTest
+
+// MARK: - Unwrap
+
+func XCTUnwrapAsync(
+ _ expression: @autoclosure () async throws -> T?,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async throws -> T {
+ let result: T?
+
+ do {
+ result = try await expression()
+ } catch {
+ return try XCTUnwrap(try { throw error }(), message(), file: file, line: line)
+ }
+ return try XCTUnwrap(result, message(), file: file, line: line)
+}
+
+// MARK: - Equality
+
+func XCTAssertEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Equatable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertEqual(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertNotEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Equatable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertNotEqual(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertNotEqual(try { () -> Bool in throw error }(), true, message(), file: file, line: line)
+ }
+}
+
+// MARK: - Fuzzy equality
+
+func XCTAssertEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ accuracy: T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Numeric {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertEqual(expr1, expr2, accuracy: accuracy, message(), file: file, line: line)
+ } catch {
+ return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertNotEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ accuracy: T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Numeric {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertNotEqual(expr1, expr2, accuracy: accuracy, message(), file: file, line: line)
+ } catch {
+ return XCTAssertNotEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ accuracy: T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: FloatingPoint {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertEqual(expr1, expr2, accuracy: accuracy, message(), file: file, line: line)
+ } catch {
+ return XCTAssertEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertNotEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ accuracy: T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: FloatingPoint {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertNotEqual(expr1, expr2, accuracy: accuracy, message(), file: file, line: line)
+ } catch {
+ return XCTAssertNotEqual(try { () -> Bool in throw error }(), false, message(), file: file, line: line)
+ }
+}
+
+// MARK: - Comparability
+
+func XCTAssertGreaterThanAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Comparable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertGreaterThan(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertGreaterThan(try { () -> Int in throw error }(), 0, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertGreaterThanOrEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Comparable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertGreaterThanOrEqual(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertGreaterThanOrEqual(try { () -> Int in throw error }(), 0, message(), file: file, line: line)
+ }
+}
+
+
+func XCTAssertLessThanAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Comparable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertLessThan(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertLessThan(try { () -> Int in throw error }(), 0, message(), file: file, line: line)
+ }
+}
+
+func XCTAssertLessThanOrEqualAsync(
+ _ expression1: @autoclosure () async throws -> T,
+ _ expression2: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async where T: Comparable {
+ do {
+ let expr1 = try await expression1(), expr2 = try await expression2()
+ return XCTAssertLessThanOrEqual(expr1, expr2, message(), file: file, line: line)
+ } catch {
+ return XCTAssertLessThanOrEqual(try { () -> Int in throw error }(), 0, message(), file: file, line: line)
+ }
+}
+
+// MARK: - Truthiness
+
+func XCTAssertAsync(
+ _ predicate: @autoclosure () async throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ let result = try await predicate()
+ XCTAssert(result, message(), file: file, line: line)
+ } catch {
+ return XCTAssert(try { throw error }(), message(), file: file, line: line)
+ }
+}
+
+func XCTAssertTrueAsync(
+ _ predicate: @autoclosure () async throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ let result = try await predicate()
+ XCTAssertTrue(result, message(), file: file, line: line)
+ } catch {
+ return XCTAssertTrue(try { throw error }(), message(), file: file, line: line)
+ }
+}
+
+func XCTAssertFalseAsync(
+ _ predicate: @autoclosure () async throws -> Bool,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ let result = try await predicate()
+ XCTAssertFalse(result, message(), file: file, line: line)
+ } catch {
+ return XCTAssertFalse(try { throw error }(), message(), file: file, line: line)
+ }
+}
+
+// MARK: - Existence
+
+func XCTAssertNilAsync(
+ _ expression: @autoclosure () async throws -> Any?,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ let result = try await expression()
+ return XCTAssertNil(result, message(), file: file, line: line)
+ } catch {
+ return XCTAssertNil(try { throw error }(), message(), file: file, line: line)
+ }
+}
+
+func XCTAssertNotNilAsync(
+ _ expression: @autoclosure () async throws -> Any?,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ let result = try await expression()
+ XCTAssertNotNil(result, message(), file: file, line: line)
+ } catch {
+ return XCTAssertNotNil(try { throw error }(), message(), file: file, line: line)
+ }
+}
+
+// MARK: - Exceptionality
+
+func XCTAssertThrowsErrorAsync(
+ _ expression: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line,
+ _ callback: (any Error) -> Void = { _ in }
+) async {
+ do {
+ _ = try await expression()
+ XCTAssertThrowsError({}(), message(), file: file, line: line, callback)
+ } catch {
+ XCTAssertThrowsError(try { throw error }(), message(), file: file, line: line, callback)
+ }
+}
+
+func XCTAssertNoThrowAsync(
+ _ expression: @autoclosure () async throws -> T,
+ _ message: @autoclosure () -> String = "",
+ file: StaticString = #filePath, line: UInt = #line
+) async {
+ do {
+ _ = try await expression()
+ } catch {
+ XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line)
+ }
+}