From 11190b81ace2b6099c0fe3a467b07c1a6eca9775 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:00:24 -0500
Subject: [PATCH 01/19] Bump minimum Swift version to 5.8. Update CI. General
package cleanup. General docs cleanup.
---
.github/dependabot.yml | 12 ---
.github/workflows/test.yml | 3 +-
Package.swift | 96 ++++++++++++-------
Package@swift-5.9.swift | 90 +++++++++++++++++
README.md | 6 +-
Sources/SQLiteNIO/Docs.docc/Documentation.md | 5 +-
.../vapor-sqlitenio-logo.svg | 0
.../SQLiteNIO/Docs.docc/theme-settings.json | 2 +-
Sources/SQLiteNIO/Exports.swift | 12 ---
9 files changed, 158 insertions(+), 68 deletions(-)
create mode 100644 Package@swift-5.9.swift
rename Sources/SQLiteNIO/Docs.docc/{images => Resources}/vapor-sqlitenio-logo.svg (100%)
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..4e5ae76 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.62.0"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
],
targets: [
.plugin(
@@ -25,38 +25,62 @@ 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"),
+ .enableUpcomingFeature("DisableOutwardActorInference"),
+ .enableExperimentalFeature("StrictConcurrency=complete"),
+] }
+
+var sqliteCSettings: [CSetting] { [
+ // 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"),
+] }
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..6dc8155
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,90 @@
+// 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.62.0"),
+ .package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
+ ],
+ 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_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"),
+] }
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
From e996789078e4a2bf5c42daf748f71131af275e27 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:03:10 -0500
Subject: [PATCH 02/19] Add explicit async versions of all ELF methods of
SQLiteConnection
---
Sources/SQLiteNIO/SQLiteConnection.swift | 137 +++++++++++++++++++----
1 file changed, 114 insertions(+), 23 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 62543b5..8ad19bc 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -7,16 +7,28 @@ public protocol SQLiteDatabase {
var logger: Logger { get }
var eventLoop: any EventLoop { get }
- @preconcurrency func query(
+ @preconcurrency
+ func query(
_ query: String,
_ binds: [SQLiteData],
logger: Logger,
_ onRow: @escaping @Sendable (SQLiteRow) -> Void
) -> EventLoopFuture
- @preconcurrency func withConnection(
+ func query(
+ _ query: String,
+ _ binds: [SQLiteData],
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) async throws
+
+ @preconcurrency
+ func withConnection(
_: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
) -> EventLoopFuture
+
+ func withConnection(
+ _: @escaping @Sendable (SQLiteConnection) async throws -> T
+ ) async throws -> T
}
extension SQLiteDatabase {
@@ -29,37 +41,70 @@ extension SQLiteDatabase {
self.query(query, binds, logger: self.logger, onRow)
}
+ 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()
+ }
+
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) { row in
rows.wrappedValue.append(row)
}.map { rows.wrappedValue }
+ #else
+ nonisolated(unsafe) var rows: [SQLiteRow] = []
+ return self.query(query, binds, logger: self.logger) { row in
+ rows.append(row)
+ }.map { rows }
+ #endif
+ }
+
+ public func query(_ query: String, _ binds: [SQLiteData] = []) async throws -> [SQLiteRow] {
+ try await self.query(query, binds).get()
}
- }
+
+ 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()
+ }
+}
extension SQLiteDatabase {
public func logging(to logger: Logger) -> any SQLiteDatabase {
- _SQLiteDatabaseCustomLogger(database: self, logger: logger)
+ SQLiteDatabaseCustomLogger(database: self, logger: logger)
}
}
-private struct _SQLiteDatabaseCustomLogger: SQLiteDatabase {
+private struct SQLiteDatabaseCustomLogger: SQLiteDatabase {
let database: any SQLiteDatabase
- var eventLoop: any EventLoop {
- self.database.eventLoop
- }
+ var eventLoop: any EventLoop { self.database.eventLoop }
let logger: Logger
- @preconcurrency func withConnection(
+ @preconcurrency
+ func withConnection(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
) -> EventLoopFuture {
self.database.withConnection(closure)
}
- @preconcurrency func query(
+ func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T) async throws -> T {
+ try await self.database.withConnection(closure)
+ }
+
+ @preconcurrency
+ func query(
_ query: String,
_ binds: [SQLiteData],
logger: Logger,
@@ -67,9 +112,13 @@ private struct _SQLiteDatabaseCustomLogger: SQLiteDatabase {
) -> EventLoopFuture {
self.database.query(query, binds, logger: logger, onRow)
}
+
+ func query(_ query: String, _ binds: [SQLiteData], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) async throws {
+ try await self.database.query(query, binds, onRow)
+ }
}
-internal final class SQLiteConnectionHandle: @unchecked Sendable {
+final class SQLiteConnectionHandle: @unchecked Sendable {
var raw: OpaquePointer?
init(_ raw: OpaquePointer?) {
@@ -77,9 +126,9 @@ internal final class SQLiteConnectionHandle: @unchecked Sendable {
}
}
-public final class SQLiteConnection: SQLiteDatabase {
+public final class SQLiteConnection: SQLiteDatabase, Sendable {
/// Available SQLite storage methods.
- public enum Storage {
+ public enum Storage: Equatable, Sendable {
/// In-memory storage. Not persisted between application launches.
/// Good for unit testing or caching.
case memory
@@ -89,11 +138,11 @@ public final class SQLiteConnection: SQLiteDatabase {
}
public let eventLoop: any EventLoop
-
- internal let handle: SQLiteConnectionHandle
- internal let threadPool: NIOThreadPool
public let logger: Logger
+ let handle: SQLiteConnectionHandle
+ let threadPool: NIOThreadPool
+
public var isClosed: Bool {
self.handle.raw == nil
}
@@ -109,6 +158,10 @@ public final class SQLiteConnection: SQLiteDatabase {
on: MultiThreadedEventLoopGroup.singleton.any()
)
}
+
+ 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())
+ }
public static func open(
storage: Storage = .memory,
@@ -143,6 +196,15 @@ public final class SQLiteConnection: SQLiteDatabase {
}
}
}
+
+ 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 Self.open(storage: storage, threadPool: threadPool, logger: logger, on: eventLoop).get()
+ }
init(
handle: OpaquePointer?,
@@ -171,7 +233,11 @@ public final class SQLiteConnection: SQLiteDatabase {
}
}
- internal var errorMessage: String? {
+ public func lastAutoincrementID() async throws -> Int {
+ try await self.lastAutoincrementID().get()
+ }
+
+ var errorMessage: String? {
if let raw = sqlite_nio_sqlite3_errmsg(self.handle.raw) {
return String(cString: raw)
} else {
@@ -179,13 +245,21 @@ public final class SQLiteConnection: SQLiteDatabase {
}
}
- @preconcurrency public func withConnection(
+ @preconcurrency
+ public func withConnection(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
) -> EventLoopFuture {
closure(self)
}
- @preconcurrency public func query(
+ public func withConnection(
+ _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
+ ) async throws -> T {
+ try await closure(self)
+ }
+
+ @preconcurrency
+ public func query(
_ query: String,
_ binds: [SQLiteData],
logger: Logger,
@@ -201,7 +275,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,6 +289,14 @@ public final class SQLiteConnection: SQLiteDatabase {
return promise.futureResult
}
+ public func query(
+ _ query: String,
+ _ binds: [SQLiteData],
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) async throws {
+ try await self.query(query, binds, onRow).get()
+ }
+
public func close() -> EventLoopFuture {
self.threadPool.runIfActive(eventLoop: self.eventLoop) {
sqlite_nio_sqlite3_close(self.handle.raw)
@@ -222,6 +304,10 @@ public final class SQLiteConnection: SQLiteDatabase {
}
}
+ public func close() async throws {
+ try await self.close().get()
+ }
+
public func install(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
logger.trace("Adding custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
@@ -229,6 +315,10 @@ public final class SQLiteConnection: SQLiteDatabase {
}
}
+ public func install(customFunction: SQLiteCustomFunction) async throws {
+ try await self.install(customFunction: customFunction).get()
+ }
+
public func uninstall(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
logger.trace("Removing custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
@@ -236,6 +326,10 @@ public final class SQLiteConnection: SQLiteDatabase {
}
}
+ public func uninstall(customFunction: SQLiteCustomFunction) async throws {
+ try await self.uninstall(customFunction: customFunction).get()
+ }
+
deinit {
assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing")
}
@@ -245,6 +339,3 @@ fileprivate final class UnsafeMutableTransferBox: @unchecked
var wrappedValue: Wrapped
init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue }
}
-
-extension SQLiteConnection: Sendable {}
-extension SQLiteConnection.Storage: Sendable {}
From f2ca49506c31924d51477103710b593a4d066d17 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:03:36 -0500
Subject: [PATCH 03/19] Improve handling of blob and text data in SQLiteData
---
Sources/SQLiteNIO/SQLiteData.swift | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift
index b93a91f..aeadd31 100644
--- a/Sources/SQLiteNIO/SQLiteData.swift
+++ b/Sources/SQLiteNIO/SQLiteData.swift
@@ -90,7 +90,7 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
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)""#
}
}
@@ -101,9 +101,7 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
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()
}
}
@@ -119,7 +117,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))
From b8ae701114c8e28b4acbeedf87d68e24879ef20b Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:04:38 -0500
Subject: [PATCH 04/19] Make `String` accept integer and real values as well as
textual values when being decoded, make `Double` and `Float` accept integer
values, use recommended APIs for translating between ByteBuffer and Data
---
Sources/SQLiteNIO/SQLiteDataConvertible.swift | 50 +++++++++----------
1 file changed, 25 insertions(+), 25 deletions(-)
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)
}
}
From 9e3ea3d2cd1d55bef1944e0159128f95ab0d6d4d Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:07:30 -0500
Subject: [PATCH 05/19] SQLiteStatement: Use sqlite_prepare_v3() instead of
_v2(). Factor out error handling. Make sure statements get finalized when
errors occur. Use sqlite3_bind_blob64() and sqlite3_bind_text64() instead of
the 32-bit ones. Simplify reading of blob data.
---
Sources/SQLiteNIO/SQLiteStatement.swift | 126 +++++++++++-------------
1 file changed, 59 insertions(+), 67 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteStatement.swift b/Sources/SQLiteNIO/SQLiteStatement.swift
index 5a7e470..5c88bf2 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 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)
+
From 1a60e8de654c60ab761f1a95f80ffc1db64a5e8c Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:07:53 -0500
Subject: [PATCH 06/19] Misc minor cleanup (Style only, nothing functional)
---
Sources/SQLiteNIO/SQLiteDataType.swift | 1 -
Sources/SQLiteNIO/SQLiteError.swift | 197 +++++++++----------------
Sources/SQLiteNIO/SQLiteRow.swift | 13 +-
3 files changed, 71 insertions(+), 140 deletions(-)
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/SQLiteError.swift b/Sources/SQLiteNIO/SQLiteError.swift
index 611b74c..9007c5f 100644
--- a/Sources/SQLiteNIO/SQLiteError.swift
+++ b/Sources/SQLiteNIO/SQLiteError.swift
@@ -6,24 +6,24 @@ 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 {
case error
case intern
case permission
@@ -62,138 +62,75 @@ 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 .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
}
}
- 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
+ default: self = .error
}
}
}
}
-
-extension SQLiteError.Reason: Sendable {}
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 {}
From 7cb2579c6e6101fe8225b799c2790062f4fc1697 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 02:11:00 -0500
Subject: [PATCH 07/19] Tests: Make all tests async. Add helper to replace use
of defer {}. Add set of async assertions. Make tests resilient against array
index range violations. Handle blobs more sensibly instead of torturing Data.
Get rid of lots of force-unwraps. Use "throws error" assertion instead of
messy do/catch clauses. Avoid crashing if opening a DB fails. Use singleton
thread pool and MTELG always.
---
.../SQLiteCustomFunctionTests.swift | 447 +++++++-----------
Tests/SQLiteNIOTests/SQLiteNIOTests.swift | 256 +++++-----
Tests/SQLiteNIOTests/XCTAsyncAssertions.swift | 266 +++++++++++
3 files changed, 572 insertions(+), 397 deletions(-)
create mode 100644 Tests/SQLiteNIOTests/XCTAsyncAssertions.swift
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..2ea1803 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,92 @@ 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() }
-
- _ = 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()
+ 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)])
- struct MyAggregate: SQLiteCustomAggregate {
- var sum: Int = 0
- mutating func step(_ values: [SQLiteData]) throws {
- sum = sum + (values.first?.integer ?? 0)
- }
+ struct MyAggregate: SQLiteCustomAggregate {
+ var sum: Int = 0
+ mutating func step(_ values: [SQLiteData]) throws {
+ self.sum += (values.first?.integer ?? 0)
+ }
- func finalize() throws -> (any SQLiteDataConvertible)? {
- sum
- }
- }
+ func finalize() throws -> (any SQLiteDataConvertible)? {
+ self.sum
+ }
+ }
- let function = SQLiteCustomFunction("my_sum", argumentCount: 1, pure: true, aggregate: MyAggregate.self)
- _ = try conn.install(customFunction: function).wait()
+ let function = SQLiteCustomFunction("my_sum", argumentCount: 1, pure: true, aggregate: MyAggregate.self)
+ try await conn.install(customFunction: function)
- 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() }
+ 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)
+ }
- let function = SQLiteCustomFunction("my_custom_function", argumentCount: 1, pure: true) { args in
- return 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()
}
- 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)
+ }
+}
From 30282779a77212548e3cd4ac77318b91a10e7357 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 16:25:15 -0500
Subject: [PATCH 08/19] Remove incorrect new protocol requirements. Add
passthroughs for the convenience methods. Factor out the `open()`
implementation so the core part can be shared by the ELF and async versions.
Don't leak sqlite3* handles if sqlite3_busy_handler() fails for some reason.
Throw more specific errors from `open()`. Don't log at error level. Use the
async version of NIOThreadPool.runIfActive() when possible.
---
Sources/SQLiteNIO/SQLiteConnection.swift | 178 +++++++++++++----------
1 file changed, 99 insertions(+), 79 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 8ad19bc..7ed6ec2 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -5,6 +5,7 @@ import Logging
public protocol SQLiteDatabase {
var logger: Logger { get }
+
var eventLoop: any EventLoop { get }
@preconcurrency
@@ -15,23 +16,14 @@ public protocol SQLiteDatabase {
_ onRow: @escaping @Sendable (SQLiteRow) -> Void
) -> EventLoopFuture
- func query(
- _ query: String,
- _ binds: [SQLiteData],
- _ onRow: @escaping @Sendable (SQLiteRow) -> Void
- ) async throws
-
@preconcurrency
func withConnection(
_: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
) -> EventLoopFuture
-
- func withConnection(
- _: @escaping @Sendable (SQLiteConnection) async throws -> T
- ) async throws -> T
}
extension SQLiteDatabase {
+ /// Logger-less version of ``query(_:_:logger:_:)``.
@preconcurrency
public func query(
_ query: String,
@@ -41,6 +33,7 @@ extension SQLiteDatabase {
self.query(query, binds, logger: self.logger, onRow)
}
+ /// Logger-less async version of ``query(_:_:logger:_:)``.
public func query(
_ query: String,
_ binds: [SQLiteData],
@@ -49,27 +42,28 @@ extension SQLiteDatabase {
try await self.query(query, binds, logger: self.logger, onRow).get()
}
+ /// Data-returning version of ``query(_:_:_:)-2zmfi``.
public func query(
_ query: String,
_ binds: [SQLiteData] = []
) -> EventLoopFuture<[SQLiteRow]> {
- #if swift(<5.10)
+ #if swift(<5.10)
let rows: UnsafeMutableTransferBox<[SQLiteRow]> = .init([])
- return self.query(query, binds, logger: self.logger) { row in
- rows.wrappedValue.append(row)
- }.map { rows.wrappedValue }
+
+ 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) { row in
- rows.append(row)
- }.map { rows }
+
+ return self.query(query, binds, logger: self.logger) { rows.append($0) }.map { rows }
#endif
}
+ /// Data-returning version of ``query(_:_:_:)-3s65n``.
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 {
@@ -81,6 +75,13 @@ extension SQLiteDatabase {
}
}
+#if swift(<5.10)
+fileprivate final class UnsafeMutableTransferBox: @unchecked Sendable {
+ var wrappedValue: Wrapped
+ init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue }
+}
+#endif
+
extension SQLiteDatabase {
public func logging(to logger: Logger) -> any SQLiteDatabase {
SQLiteDatabaseCustomLogger(database: self, logger: logger)
@@ -92,30 +93,34 @@ private struct SQLiteDatabaseCustomLogger: SQLiteDatabase {
var eventLoop: any EventLoop { self.database.eventLoop }
let logger: Logger
- @preconcurrency
- func withConnection(
- _ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
- ) -> EventLoopFuture {
+ func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture {
self.database.withConnection(closure)
}
-
func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T) async throws -> T {
try await self.database.withConnection(closure)
}
- @preconcurrency
- func query(
- _ query: String,
- _ binds: [SQLiteData],
- logger: Logger,
- _ onRow: @escaping @Sendable (SQLiteRow) -> Void
- ) -> EventLoopFuture {
+ func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
self.database.query(query, binds, logger: logger, onRow)
}
+ func query(_ query: String, _ binds: [SQLiteData] = [], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
+ self.database.query(query, binds, onRow)
+ }
func query(_ query: String, _ binds: [SQLiteData], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) async throws {
try await self.database.query(query, binds, onRow)
}
+
+ func query(_ query: String, _ binds: [SQLiteData] = []) -> EventLoopFuture<[SQLiteRow]> {
+ self.database.query(query, binds)
+ }
+ func query(_ query: String, _ binds: [SQLiteData] = []) async throws -> [SQLiteRow] {
+ try await self.database.query(query, binds)
+ }
+
+ func logging(to logger: Logger) -> any SQLiteDatabase {
+ Self(database: self.database, logger: logger)
+ }
}
final class SQLiteConnectionHandle: @unchecked Sendable {
@@ -137,7 +142,10 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
case file(path: String)
}
+ // See `SQLiteDatabase.eventLoop`.
public let eventLoop: any EventLoop
+
+ // See `SQLiteDatabase.logger`.
public let logger: Logger
let handle: SQLiteConnectionHandle
@@ -159,41 +167,26 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
)
}
- 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())
+ 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()
+ )
}
-
+
public static func open(
storage: Storage = .memory,
threadPool: NIOThreadPool,
logger: Logger = .init(label: "codes.vapor.sqlite"),
on eventLoop: any EventLoop
) -> EventLoopFuture {
- let path: String
- switch storage {
- 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)")
- }
+ threadPool.runIfActive(eventLoop: eventLoop) {
+ try self.openInternal(storage: storage, threadPool: threadPool, logger: logger, eventLoop: eventLoop)
}
}
@@ -203,7 +196,33 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
logger: Logger = .init(label: "codes.vapor.sqlite"),
on eventLoop: any EventLoop
) async throws -> SQLiteConnection {
- try await Self.open(storage: storage, threadPool: threadPool, logger: logger, on: eventLoop).get()
+ try await threadPool.runIfActive {
+ try self.openInternal(storage: storage, threadPool: threadPool, logger: logger, eventLoop: eventLoop)
+ }
+ }
+
+ 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
+ }
+
+ var handle: OpaquePointer?
+ let openOptions = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI
+ 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 db: \(path)")
+ return SQLiteConnection(handle: handle, threadPool: threadPool, logger: logger, on: eventLoop)
}
init(
@@ -218,6 +237,10 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
self.eventLoop = eventLoop
}
+ var errorMessage: String? {
+ sqlite_nio_sqlite3_errmsg(self.handle.raw).map { String(cString: $0) }
+ }
+
public static func libraryVersion() -> Int32 {
sqlite_nio_sqlite3_libversion_number()
}
@@ -228,23 +251,16 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
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))
}
}
public func lastAutoincrementID() async throws -> Int {
- try await self.lastAutoincrementID().get()
- }
-
- var errorMessage: String? {
- if let raw = sqlite_nio_sqlite3_errmsg(self.handle.raw) {
- return String(cString: raw)
- } else {
- return nil
+ try await self.threadPool.runIfActive {
+ numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
}
}
-
+
@preconcurrency
public func withConnection(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
@@ -305,37 +321,41 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
public func close() async throws {
- try await self.close().get()
+ try await self.threadPool.runIfActive {
+ sqlite_nio_sqlite3_close(self.handle.raw)
+ self.handle.raw = nil
+ }
}
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)
}
}
public func install(customFunction: SQLiteCustomFunction) async throws {
- try await self.install(customFunction: customFunction).get()
+ self.logger.trace("Adding custom function \(customFunction.name)")
+ return try await self.threadPool.runIfActive {
+ try customFunction.install(in: self)
+ }
}
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)
}
}
public func uninstall(customFunction: SQLiteCustomFunction) async throws {
- try await self.uninstall(customFunction: customFunction).get()
+ self.logger.trace("Removing custom function \(customFunction.name)")
+ return try await self.threadPool.runIfActive {
+ try customFunction.uninstall(in: self)
+ }
}
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 }
-}
From c51dcfe254e6f5a162357b97d3ccb938829b7b6b Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 2 May 2024 16:26:32 -0500
Subject: [PATCH 09/19] Split the SQLDatabase protocol into its own file. Group
SQLiteConnnection's async methods together.
---
Sources/SQLiteNIO/SQLiteConnection.swift | 244 ++++++-----------------
Sources/SQLiteNIO/SQLiteDatabase.swift | 124 ++++++++++++
2 files changed, 187 insertions(+), 181 deletions(-)
create mode 100644 Sources/SQLiteNIO/SQLiteDatabase.swift
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 7ed6ec2..6e4dce4 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -3,126 +3,6 @@ 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 {
- /// Logger-less version of ``query(_:_:logger:_:)``.
- @preconcurrency
- public func query(
- _ query: String,
- _ binds: [SQLiteData] = [],
- _ onRow: @escaping @Sendable (SQLiteRow) -> Void
- ) -> EventLoopFuture {
- self.query(query, binds, logger: self.logger, onRow)
- }
-
- /// Logger-less async version of ``query(_:_:logger:_:)``.
- 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()
- }
-
- /// Data-returning version of ``query(_:_:_:)-2zmfi``.
- 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
- }
-
- /// Data-returning version of ``query(_:_:_:)-3s65n``.
- 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)
-fileprivate final class UnsafeMutableTransferBox: @unchecked Sendable {
- var wrappedValue: Wrapped
- init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue }
-}
-#endif
-
-extension SQLiteDatabase {
- public func logging(to logger: Logger) -> any SQLiteDatabase {
- SQLiteDatabaseCustomLogger(database: self, logger: logger)
- }
-}
-
-private struct SQLiteDatabaseCustomLogger: SQLiteDatabase {
- let database: any SQLiteDatabase
- var eventLoop: any EventLoop { self.database.eventLoop }
- let logger: Logger
-
- func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture {
- self.database.withConnection(closure)
- }
- func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T) async throws -> T {
- try await self.database.withConnection(closure)
- }
-
- func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
- self.database.query(query, binds, logger: logger, onRow)
- }
-
- func query(_ query: String, _ binds: [SQLiteData] = [], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
- self.database.query(query, binds, onRow)
- }
- func query(_ query: String, _ binds: [SQLiteData], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) async throws {
- try await self.database.query(query, binds, onRow)
- }
-
- func query(_ query: String, _ binds: [SQLiteData] = []) -> EventLoopFuture<[SQLiteRow]> {
- self.database.query(query, binds)
- }
- func query(_ query: String, _ binds: [SQLiteData] = []) async throws -> [SQLiteRow] {
- try await self.database.query(query, binds)
- }
-
- func logging(to logger: Logger) -> any SQLiteDatabase {
- Self(database: self.database, logger: logger)
- }
-}
-
final class SQLiteConnectionHandle: @unchecked Sendable {
var raw: OpaquePointer?
@@ -149,7 +29,7 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
public let logger: Logger
let handle: SQLiteConnectionHandle
- let threadPool: NIOThreadPool
+ private let threadPool: NIOThreadPool
public var isClosed: Bool {
self.handle.raw == nil
@@ -167,18 +47,6 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
)
}
- 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()
- )
- }
-
public static func open(
storage: Storage = .memory,
threadPool: NIOThreadPool,
@@ -190,17 +58,6 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
- 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)
- }
- }
-
private static func openInternal(storage: Storage, threadPool: NIOThreadPool, logger: Logger, eventLoop: any EventLoop) throws -> SQLiteConnection {
let path: String
switch storage {
@@ -255,12 +112,6 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
- public func lastAutoincrementID() async throws -> Int {
- try await self.threadPool.runIfActive {
- numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
- }
- }
-
@preconcurrency
public func withConnection(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
@@ -268,12 +119,6 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
closure(self)
}
- public func withConnection(
- _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
- ) async throws -> T {
- try await closure(self)
- }
-
@preconcurrency
public func query(
_ query: String,
@@ -305,6 +150,68 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
return promise.futureResult
}
+ public func close() -> EventLoopFuture {
+ self.threadPool.runIfActive(eventLoop: self.eventLoop) {
+ sqlite_nio_sqlite3_close(self.handle.raw)
+ self.handle.raw = nil
+ }
+ }
+
+ public func install(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
+ self.logger.trace("Adding custom function \(customFunction.name)")
+ return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
+ try customFunction.install(in: self)
+ }
+ }
+
+ public func uninstall(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
+ self.logger.trace("Removing custom function \(customFunction.name)")
+ return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
+ try customFunction.uninstall(in: self)
+ }
+ }
+
+ deinit {
+ assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing")
+ }
+}
+
+extension SQLiteConnection {
+ 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()
+ )
+ }
+
+ 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)
+ }
+ }
+
+ public func lastAutoincrementID() async throws -> Int {
+ try await self.threadPool.runIfActive {
+ numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
+ }
+ }
+
+ public func withConnection(
+ _ closure: @escaping @Sendable (SQLiteConnection) async throws -> T
+ ) async throws -> T {
+ try await closure(self)
+ }
+
public func query(
_ query: String,
_ binds: [SQLiteData],
@@ -313,13 +220,6 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
try await self.query(query, binds, onRow).get()
}
- public func close() -> EventLoopFuture {
- self.threadPool.runIfActive(eventLoop: self.eventLoop) {
- sqlite_nio_sqlite3_close(self.handle.raw)
- self.handle.raw = nil
- }
- }
-
public func close() async throws {
try await self.threadPool.runIfActive {
sqlite_nio_sqlite3_close(self.handle.raw)
@@ -327,13 +227,6 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
- public func install(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
- self.logger.trace("Adding custom function \(customFunction.name)")
- return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
- try customFunction.install(in: self)
- }
- }
-
public func install(customFunction: SQLiteCustomFunction) async throws {
self.logger.trace("Adding custom function \(customFunction.name)")
return try await self.threadPool.runIfActive {
@@ -341,21 +234,10 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
- public func uninstall(customFunction: SQLiteCustomFunction) -> EventLoopFuture {
- self.logger.trace("Removing custom function \(customFunction.name)")
- return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
- try customFunction.uninstall(in: self)
- }
- }
-
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)
}
}
-
- deinit {
- assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing")
- }
}
diff --git a/Sources/SQLiteNIO/SQLiteDatabase.swift b/Sources/SQLiteNIO/SQLiteDatabase.swift
new file mode 100644
index 0000000..fdb6a6b
--- /dev/null
+++ b/Sources/SQLiteNIO/SQLiteDatabase.swift
@@ -0,0 +1,124 @@
+import NIOCore
+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 {
+ /// Logger-less version of ``query(_:_:logger:_:)``.
+ @preconcurrency
+ public func query(
+ _ query: String,
+ _ binds: [SQLiteData] = [],
+ _ onRow: @escaping @Sendable (SQLiteRow) -> Void
+ ) -> EventLoopFuture {
+ self.query(query, binds, logger: self.logger, onRow)
+ }
+
+ /// Logger-less async version of ``query(_:_:logger:_:)``.
+ 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()
+ }
+
+ /// Data-returning version of ``query(_:_:_:)-2zmfi``.
+ 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
+ }
+
+ /// Data-returning version of ``query(_:_:_:)-3s65n``.
+ 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)
+fileprivate final class UnsafeMutableTransferBox: @unchecked Sendable {
+ var wrappedValue: Wrapped
+ init(_ wrappedValue: Wrapped) { self.wrappedValue = wrappedValue }
+}
+#endif
+
+extension SQLiteDatabase {
+ public func logging(to logger: Logger) -> any SQLiteDatabase {
+ SQLiteDatabaseCustomLogger(database: self, logger: logger)
+ }
+}
+
+private struct SQLiteDatabaseCustomLogger: SQLiteDatabase {
+ let database: any SQLiteDatabase
+ var eventLoop: any EventLoop { self.database.eventLoop }
+ let logger: Logger
+
+ func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture) -> EventLoopFuture {
+ self.database.withConnection(closure)
+ }
+ func withConnection(_ closure: @escaping @Sendable (SQLiteConnection) async throws -> T) async throws -> T {
+ try await self.database.withConnection(closure)
+ }
+
+ func query(_ query: String, _ binds: [SQLiteData], logger: Logger, _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
+ self.database.query(query, binds, logger: logger, onRow)
+ }
+
+ func query(_ query: String, _ binds: [SQLiteData] = [], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) -> EventLoopFuture {
+ self.database.query(query, binds, onRow)
+ }
+ func query(_ query: String, _ binds: [SQLiteData], _ onRow: @escaping @Sendable (SQLiteRow) -> Void) async throws {
+ try await self.database.query(query, binds, onRow)
+ }
+
+ func query(_ query: String, _ binds: [SQLiteData] = []) -> EventLoopFuture<[SQLiteRow]> {
+ self.database.query(query, binds)
+ }
+ func query(_ query: String, _ binds: [SQLiteData] = []) async throws -> [SQLiteRow] {
+ try await self.database.query(query, binds)
+ }
+
+ func logging(to logger: Logger) -> any SQLiteDatabase {
+ Self(database: self.database, logger: logger)
+ }
+}
From 1c93f9400e47ef227cc820513dfc4b14fd15525b Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Fri, 3 May 2024 08:09:34 -0500
Subject: [PATCH 10/19] We always pass SQLITE_OPEN_FULLMUTEX (serialized mode)
to sqlite3_open_v2(), so there's no point in setting multithread mode as the
default during compilation; use serialized as the default instead.
---
Package.swift | 2 +-
Package@swift-5.9.swift | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/Package.swift b/Package.swift
index 4e5ae76..9a92114 100644
--- a/Package.swift
+++ b/Package.swift
@@ -81,6 +81,6 @@ var sqliteCSettings: [CSetting] { [
.define("SQLITE_OMIT_LOAD_EXTENSION"),
.define("SQLITE_OMIT_SHARED_CACHE"),
.define("SQLITE_SECURE_DELETE"),
- .define("SQLITE_THREADSAFE", to: "2"),
+ .define("SQLITE_THREADSAFE", to: "1"),
.define("SQLITE_USE_URI"),
] }
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
index 6dc8155..aa3d76a 100644
--- a/Package@swift-5.9.swift
+++ b/Package@swift-5.9.swift
@@ -85,6 +85,6 @@ var sqliteCSettings: [CSetting] { [
.define("SQLITE_OMIT_LOAD_EXTENSION"),
.define("SQLITE_OMIT_SHARED_CACHE"),
.define("SQLITE_SECURE_DELETE"),
- .define("SQLITE_THREADSAFE", to: "2"),
+ .define("SQLITE_THREADSAFE", to: "1"),
.define("SQLITE_USE_URI"),
] }
From 2566f0425705ea1902428f9a1fb9567d7c7656b5 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Fri, 3 May 2024 08:13:52 -0500
Subject: [PATCH 11/19] Add test that validates we're using SQLite in the
correct (safest) threading mode.
---
Tests/SQLiteNIOTests/SQLiteNIOTests.swift | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift
index 2ea1803..bfe2162 100644
--- a/Tests/SQLiteNIOTests/SQLiteNIOTests.swift
+++ b/Tests/SQLiteNIOTests/SQLiteNIOTests.swift
@@ -172,6 +172,27 @@ final class SQLiteNIOTests: XCTestCase {
await XCTAssertNoThrowAsync(conn = try await SQLiteConnection.open(storage: .memory).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
+ }
+ }
override class func setUp() {
XCTAssert(isLoggingConfigured)
From 89e319fcd0c032c4cd96f37fb4ca465f7ebd26f7 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Sat, 4 May 2024 08:30:12 -0500
Subject: [PATCH 12/19] Fix a couple of log messages
---
Sources/SQLiteNIO/SQLiteStatement.swift | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteStatement.swift b/Sources/SQLiteNIO/SQLiteStatement.swift
index 5c88bf2..e32c897 100644
--- a/Sources/SQLiteNIO/SQLiteStatement.swift
+++ b/Sources/SQLiteNIO/SQLiteStatement.swift
@@ -111,13 +111,13 @@ struct SQLiteStatement {
case SQLITE_NULL:
return .null
default:
- throw SQLiteError(reason: .error, message: "Unexpected column type.")
+ 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: "Unexpectedly nil column name at offset \(offset)")
+ throw SQLiteError(reason: .error, message: "Unexpectedly found a nil column name at offset \(offset)")
}
return String(cString: cName)
}
From ccc22b564089152bf9393a0052e85864ba0eb389 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Sat, 4 May 2024 08:31:07 -0500
Subject: [PATCH 13/19] Apply several SQLite compilation settings according to
upstream documentation, plus omitting several APIs that aren't usable through
this package anyway.
---
Package.swift | 15 ++++++++++++---
Package@swift-5.9.swift | 15 ++++++++++++---
2 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/Package.swift b/Package.swift
index 9a92114..31c0eec 100644
--- a/Package.swift
+++ b/Package.swift
@@ -59,8 +59,10 @@ var swiftSettings: [SwiftSetting] { [
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"),
+ .define("SQLITE_ENABLE_API_ARMOR", .when(configuration: .debug)),
.define("SQLITE_ENABLE_COLUMN_METADATA"),
.define("SQLITE_ENABLE_DBSTAT_VTAB"),
.define("SQLITE_ENABLE_FTS3"),
@@ -68,8 +70,7 @@ var sqliteCSettings: [CSetting] { [
.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_NULL_TRIM"),
.define("SQLITE_ENABLE_RTREE"),
.define("SQLITE_ENABLE_SESSION"),
.define("SQLITE_ENABLE_STMTVTAB"),
@@ -77,10 +78,18 @@ var sqliteCSettings: [CSetting] { [
.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
index aa3d76a..ffccdfb 100644
--- a/Package@swift-5.9.swift
+++ b/Package@swift-5.9.swift
@@ -63,8 +63,10 @@ var swiftSettings: [SwiftSetting] { [
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"),
+ .define("SQLITE_ENABLE_API_ARMOR", .when(configuration: .debug)),
.define("SQLITE_ENABLE_COLUMN_METADATA"),
.define("SQLITE_ENABLE_DBSTAT_VTAB"),
.define("SQLITE_ENABLE_FTS3"),
@@ -72,8 +74,7 @@ var sqliteCSettings: [CSetting] { [
.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_NULL_TRIM"),
.define("SQLITE_ENABLE_RTREE"),
.define("SQLITE_ENABLE_SESSION"),
.define("SQLITE_ENABLE_STMTVTAB"),
@@ -81,10 +82,18 @@ var sqliteCSettings: [CSetting] { [
.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"),
] }
From 0e5ca1ae1c0f53a9b5f0c5d9064fc3f454e9f917 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Sat, 4 May 2024 08:32:06 -0500
Subject: [PATCH 14/19] As recommended in the docs, mark custom functions
SQLITE_DIRECTONLY by default (security hardening). Provide an initializer
flag to override it if needed.
---
Sources/SQLiteNIO/SQLiteCustomFunction.swift | 48 ++++++++++----------
1 file changed, 24 insertions(+), 24 deletions(-)
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)
From 8c9aa9b768a3870663df084a191963f1f39e207a Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Sat, 4 May 2024 12:02:44 -0500
Subject: [PATCH 15/19] Remove not yet available upcoming/experimental feature
flags from the 5.8 manifest
---
Package.swift | 2 --
1 file changed, 2 deletions(-)
diff --git a/Package.swift b/Package.swift
index 31c0eec..3cfcbdf 100644
--- a/Package.swift
+++ b/Package.swift
@@ -53,8 +53,6 @@ let package = Package(
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("ForwardTrailingClosures"),
- .enableUpcomingFeature("DisableOutwardActorInference"),
- .enableExperimentalFeature("StrictConcurrency=complete"),
] }
var sqliteCSettings: [CSetting] { [
From 6daa67b3c195e61e37ddaf1a4fd428d5e3ef4c12 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 9 May 2024 21:37:04 -0500
Subject: [PATCH 16/19] Update dependency minimums
---
Package.swift | 4 ++--
Package@swift-5.9.swift | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/Package.swift b/Package.swift
index 3cfcbdf..d7d6fe4 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,8 +13,8 @@ let package = Package(
.library(name: "SQLiteNIO", targets: ["SQLiteNIO"]),
],
dependencies: [
- .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
- .package(url: "https://github.com/apple/swift-log.git", from: "1.5.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(
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
index ffccdfb..9aa260b 100644
--- a/Package@swift-5.9.swift
+++ b/Package@swift-5.9.swift
@@ -13,8 +13,8 @@ let package = Package(
.library(name: "SQLiteNIO", targets: ["SQLiteNIO"]),
],
dependencies: [
- .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
- .package(url: "https://github.com/apple/swift-log.git", from: "1.5.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(
From 6f72ea1c5787634ea6b49a666e76cb431e030cbd Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 9 May 2024 21:56:41 -0500
Subject: [PATCH 17/19] Add support for SQLite's extended result codes
---
Sources/SQLiteNIO/SQLiteConnection.swift | 2 +-
Sources/SQLiteNIO/SQLiteError.swift | 232 ++++++++++++++++++++++-
2 files changed, 231 insertions(+), 3 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 6e4dce4..41485ad 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -66,7 +66,7 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
var handle: OpaquePointer?
- let openOptions = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_URI
+ 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)")
diff --git a/Sources/SQLiteNIO/SQLiteError.swift b/Sources/SQLiteNIO/SQLiteError.swift
index 9007c5f..57deaa8 100644
--- a/Sources/SQLiteNIO/SQLiteError.swift
+++ b/Sources/SQLiteNIO/SQLiteError.swift
@@ -24,6 +24,7 @@ public struct SQLiteError: Error, CustomStringConvertible, LocalizedError {
}
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
@@ -92,8 +120,69 @@ public struct SQLiteError: Error, CustomStringConvertible, LocalizedError {
case .warning: return SQLITE_WARNING
case .row: return SQLITE_ROW
case .done: return SQLITE_DONE
- case .connection, .close, .prepare, .bind, .execute:
- return -1
+ 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
}
}
@@ -129,8 +218,147 @@ public struct SQLiteError: Error, CustomStringConvertible, LocalizedError {
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
}
}
}
}
+
+/// 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))
From 84c80bdf6b3119df41f292e7355241b7205de97b Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 9 May 2024 21:58:06 -0500
Subject: [PATCH 18/19] Lots of documentation comments. Reorganize
SQLiteConnection a little. SQLiteDatabase is Sendable.
---
Sources/SQLiteNIO/SQLiteConnection.swift | 193 ++++++++++++++++++++---
Sources/SQLiteNIO/SQLiteData.swift | 47 ++++--
Sources/SQLiteNIO/SQLiteDatabase.swift | 102 ++++++++++--
3 files changed, 291 insertions(+), 51 deletions(-)
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 41485ad..2264b8e 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -3,6 +3,33 @@ import NIOPosix
import CSQLite
import Logging
+/// 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".
final class SQLiteConnectionHandle: @unchecked Sendable {
var raw: OpaquePointer?
@@ -11,30 +38,59 @@ final class SQLiteConnectionHandle: @unchecked Sendable {
}
}
+/// Represents a single open connection to an SQLite database, either on disk or in memory.
public final class SQLiteConnection: SQLiteDatabase, Sendable {
- /// Available SQLite storage methods.
+ /// The possible storage types for an SQLite database.
public enum Storage: Equatable, Sendable {
- /// In-memory storage. Not persisted between application launches.
- /// Good for unit testing or caching.
+ /// 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)
}
- // See `SQLiteDatabase.eventLoop`.
- public let eventLoop: any EventLoop
-
- // See `SQLiteDatabase.logger`.
- public let logger: Logger
-
- let handle: SQLiteConnectionHandle
- private let threadPool: NIOThreadPool
+ /// 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")
@@ -47,6 +103,14 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
)
}
+ /// 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,
@@ -58,7 +122,14 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
- private static func openInternal(storage: Storage, threadPool: NIOThreadPool, logger: Logger, eventLoop: any EventLoop) throws -> SQLiteConnection {
+ /// 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:"
@@ -78,11 +149,24 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
throw SQLiteError(reason: .init(statusCode: busyRet), message: "Failed to set busy handler for SQLite database at \(path)")
}
- logger.debug("Connected to sqlite db: \(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,
@@ -94,24 +178,31 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
self.eventLoop = eventLoop
}
+ /// 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 libraryVersion() -> Int32 {
- sqlite_nio_sqlite3_libversion_number()
- }
-
- 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) {
numericCast(sqlite_nio_sqlite3_last_insert_rowid(self.handle.raw))
}
}
+ // See `SQLiteDatabase.withConnection(_:)`.
@preconcurrency
public func withConnection(
_ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
@@ -119,6 +210,7 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
closure(self)
}
+ // See `SQLiteDatabase.query(_:_:logger:_:)`.
@preconcurrency
public func query(
_ query: String,
@@ -150,13 +242,22 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
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 {
self.logger.trace("Adding custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
@@ -164,6 +265,10 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
+ /// 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 {
self.logger.trace("Removing custom function \(customFunction.name)")
return self.threadPool.runIfActive(eventLoop: self.eventLoop) {
@@ -171,12 +276,23 @@ public final class SQLiteConnection: SQLiteDatabase, Sendable {
}
}
+ /// Deinitializer for ``SQLiteConnection``.
deinit {
assert(self.handle.raw == nil, "SQLiteConnection was not closed before deinitializing")
}
}
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")
@@ -189,6 +305,14 @@ extension SQLiteConnection {
)
}
+ /// 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,
@@ -200,18 +324,26 @@ extension SQLiteConnection {
}
}
+ /// 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],
@@ -220,6 +352,9 @@ extension SQLiteConnection {
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)
@@ -227,6 +362,9 @@ extension SQLiteConnection {
}
}
+ /// 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 {
@@ -234,6 +372,9 @@ extension SQLiteConnection {
}
}
+ /// 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 {
diff --git a/Sources/SQLiteNIO/SQLiteData.swift b/Sources/SQLiteNIO/SQLiteData.swift
index aeadd31..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,7 +106,7 @@ 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>"
@@ -94,7 +117,7 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
}
- /// See `Encodable`.
+ // See `Encodable.encode(to:)`.
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
@@ -108,6 +131,7 @@ public enum SQLiteData: Equatable, Encodable, CustomStringConvertible {
}
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:
@@ -135,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/SQLiteDatabase.swift b/Sources/SQLiteNIO/SQLiteDatabase.swift
index fdb6a6b..539a182 100644
--- a/Sources/SQLiteNIO/SQLiteDatabase.swift
+++ b/Sources/SQLiteNIO/SQLiteDatabase.swift
@@ -3,11 +3,36 @@ import NIOPosix
import CSQLite
import Logging
-public protocol SQLiteDatabase {
+/// 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,
@@ -16,14 +41,27 @@ public protocol SQLiteDatabase {
_ 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(
- _: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
+ _ closure: @escaping @Sendable (SQLiteConnection) -> EventLoopFuture
) -> EventLoopFuture
}
+/// Convenience helpers and Concurrency-aware variants.
extension SQLiteDatabase {
- /// Logger-less version of ``query(_:_:logger:_:)``.
+ /// 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,
@@ -33,20 +71,20 @@ extension SQLiteDatabase {
self.query(query, binds, logger: self.logger, onRow)
}
- /// Logger-less async version of ``query(_:_:logger:_:)``.
+ /// 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],
+ _ binds: [SQLiteData] = [],
_ onRow: @escaping @Sendable (SQLiteRow) -> Void
) async throws {
try await self.query(query, binds, logger: self.logger, onRow).get()
}
- /// Data-returning version of ``query(_:_:_:)-2zmfi``.
- public func query(
- _ query: String,
- _ binds: [SQLiteData] = []
- ) -> EventLoopFuture<[SQLiteRow]> {
+ /// 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([])
@@ -58,7 +96,8 @@ extension SQLiteDatabase {
#endif
}
- /// Data-returning version of ``query(_:_:_:)-3s65n``.
+ /// 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()
}
@@ -76,6 +115,9 @@ extension SQLiteDatabase {
}
#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 }
@@ -83,42 +125,74 @@ fileprivate final class UnsafeMutableTransferBox: @unchecked
#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)
}
}
-private struct SQLiteDatabaseCustomLogger: SQLiteDatabase {
- let database: any SQLiteDatabase
- var eventLoop: any EventLoop { self.database.eventLoop }
+/// 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)
}
}
From 575470b8a08176e3763bae12261b344f5cd5d145 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Fri, 10 May 2024 01:22:25 -0500
Subject: [PATCH 19/19] Try to silence TSan false positives in 5.8
---
Sources/SQLiteNIO/SQLiteConnection.swift | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/Sources/SQLiteNIO/SQLiteConnection.swift b/Sources/SQLiteNIO/SQLiteConnection.swift
index 2264b8e..2b0f306 100644
--- a/Sources/SQLiteNIO/SQLiteConnection.swift
+++ b/Sources/SQLiteNIO/SQLiteConnection.swift
@@ -1,4 +1,7 @@
import NIOCore
+#if swift(<5.9)
+import NIOConcurrencyHelpers
+#endif
import NIOPosix
import CSQLite
import Logging
@@ -30,12 +33,27 @@ import Logging
/// 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 } }
+ }
+
+ init(_ raw: OpaquePointer?) {
+ self._raw = .init(raw)
+ }
+ #else
var raw: OpaquePointer?
init(_ raw: OpaquePointer?) {
self.raw = raw
}
+ #endif
}
/// Represents a single open connection to an SQLite database, either on disk or in memory.