diff --git a/Package.swift b/Package.swift
index d1ccfb3..7ef451d 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,12 +13,14 @@ let package = Package(
.library(name: "SQLiteKit", targets: ["SQLiteKit"]),
],
dependencies: [
- .package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.6.0"),
+ .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
+ .package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.8.4"),
.package(url: "https://github.com/vapor/sql-kit.git", from: "3.28.0"),
- .package(url: "https://github.com/vapor/async-kit.git", from: "1.14.0"),
+ .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"),
],
targets: [
.target(name: "SQLiteKit", dependencies: [
+ .product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "AsyncKit", package: "async-kit"),
.product(name: "SQLiteNIO", package: "sqlite-nio"),
.product(name: "SQLKit", package: "sql-kit"),
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..43bd242
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,46 @@
+// swift-tools-version:5.9
+import PackageDescription
+
+let swiftSettings: [SwiftSetting] = [
+ .enableUpcomingFeature("ExistentialAny"),
+ .enableExperimentalFeature("StrictConcurrency=complete"),
+]
+
+let package = Package(
+ name: "sqlite-kit",
+ platforms: [
+ .macOS(.v10_15),
+ .iOS(.v13),
+ .watchOS(.v6),
+ .tvOS(.v13),
+ ],
+ products: [
+ .library(name: "SQLiteKit", targets: ["SQLiteKit"]),
+ ],
+ dependencies: [
+ .package(url: "https://github.com/apple/swift-nio.git", from: "2.62.0"),
+ .package(url: "https://github.com/vapor/sqlite-nio.git", from: "1.8.4"),
+ .package(url: "https://github.com/vapor/sql-kit.git", from: "3.28.0"),
+ .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"),
+ ],
+ targets: [
+ .target(
+ name: "SQLiteKit",
+ dependencies: [
+ .product(name: "NIOFoundationCompat", package: "swift-nio"),
+ .product(name: "AsyncKit", package: "async-kit"),
+ .product(name: "SQLiteNIO", package: "sqlite-nio"),
+ .product(name: "SQLKit", package: "sql-kit"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ .testTarget(
+ name: "SQLiteKitTests",
+ dependencies: [
+ .product(name: "SQLKitBenchmark", package: "sql-kit"),
+ .target(name: "SQLiteKit"),
+ ],
+ swiftSettings: swiftSettings
+ ),
+ ]
+)
diff --git a/README.md b/README.md
index dca42c2..9102bb7 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,28 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+SQLiteKit is a library providing an [SQLKit] driver for [SQLiteNIO].
+
+> [!NOTE]
+> The [FluentKit] driver for SQLite is provided by the [FluentSQLiteDriver] package.
+
+[SQLKit]: https://swiftpackageindex.com/vapor/sql-kit
+[SQLiteNIO]: https://swiftpackageindex.com/vapor/sqlite-nio
+[Fluent]: https://swiftpackageindex.com/vapor/fluent-kit
+[FluentSQLiteDriver]: https://swiftpackageindex.com/vapor/fluent-sqlite-driver
diff --git a/Sources/SQLiteKit/Docs.docc/images/article.svg b/Sources/SQLiteKit/Docs.docc/images/article.svg
deleted file mode 100644
index 3dc6a66..0000000
--- a/Sources/SQLiteKit/Docs.docc/images/article.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/Sources/SQLiteKit/Docs.docc/images/vapor-sqlite-logo.svg b/Sources/SQLiteKit/Docs.docc/images/vapor-sqlite-logo.svg
deleted file mode 100644
index e87cade..0000000
--- a/Sources/SQLiteKit/Docs.docc/images/vapor-sqlite-logo.svg
+++ /dev/null
@@ -1,58 +0,0 @@
-
diff --git a/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg b/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg
new file mode 100644
index 0000000..a3d3287
--- /dev/null
+++ b/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg
@@ -0,0 +1,22 @@
+
diff --git a/Sources/SQLiteKit/Docs.docc/theme-settings.json b/Sources/SQLiteKit/Docs.docc/theme-settings.json
index 437875c..806ccb9 100644
--- a/Sources/SQLiteKit/Docs.docc/theme-settings.json
+++ b/Sources/SQLiteKit/Docs.docc/theme-settings.json
@@ -1,46 +1,21 @@
{
- "theme": {
- "aside": {
- "border-radius": "6px",
- "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"
- },
- "color": {
- "fill": {
- "dark": "rgb(0, 0, 0)",
- "light": "rgb(255, 255, 255)"
- },
- "sqlite-teal": "hsl(215, 45%, 58%)",
- "documentation-intro-fill": "radial-gradient(circle at top, var(--color-documentation-intro-accent) 30%, #000 100%)",
- "documentation-intro-accent": "var(--color-sqlite-teal)",
- "documentation-intro-accent-outer": {
- "dark": "rgb(255, 255, 255)",
- "light": "rgb(0, 0, 0)"
- },
- "documentation-intro-accent-inner": {
- "dark": "rgb(0, 0, 0)",
- "light": "rgb(255, 255, 255)"
- }
- },
- "icons": {
- "technology": "/sqlitekit/images/vapor-sqlite-logo.svg",
- "article": "/sqlitekit/images/article.svg"
- }
+ "theme": {
+ "aside": { "border-radius": "6px", "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" },
+ "color": {
+ "sqlite": "hsl(215, 45%, 58%)",
+ "documentation-intro-fill": "radial-gradient(circle at top, var(--color-sqlite) 30%, #000 100%)",
+ "documentation-intro-accent": "var(--color-sqlite)",
+ "logo-base": { "dark": "#fff", "light": "#000" },
+ "logo-shape": { "dark": "#000", "light": "#fff" },
+ "fill": { "dark": "#000", "light": "#fff" }
},
- "features": {
- "quickNavigation": {
- "enable": true
- }
- }
+ "icons": { "technology": "/sqlitekit/images/vapor-sqlitekit-logo.svg" }
+ },
+ "features": {
+ "quickNavigation": { "enable": true },
+ "i18n": { "enable": true }
+ }
}
diff --git a/Sources/SQLiteKit/SQLiteConfiguration.swift b/Sources/SQLiteKit/SQLiteConfiguration.swift
index 0a11cf8..9177522 100644
--- a/Sources/SQLiteKit/SQLiteConfiguration.swift
+++ b/Sources/SQLiteKit/SQLiteConfiguration.swift
@@ -1,7 +1,7 @@
import struct Foundation.UUID
-public struct SQLiteConfiguration {
- public enum Storage {
+public struct SQLiteConfiguration: Sendable {
+ public enum Storage: Sendable {
/// Stores the SQLite database in memory.
///
/// Uses a randomly generated identifier. See `memory(identifier:)`.
diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
index 3c2cd85..bde2384 100644
--- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
+++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
@@ -82,7 +82,7 @@ internal struct _SQLiteDatabaseVersion: SQLDatabaseReportedVersion {
}
private struct _SQLiteSQLDatabase: SQLDatabase {
- let database: SQLiteDatabase
+ let database: any SQLiteDatabase
var eventLoop: any EventLoop {
self.database.eventLoop
@@ -109,17 +109,18 @@ private struct _SQLiteSQLDatabase: SQLDatabase {
let binds: [SQLiteData]
do {
binds = try serializer.binds.map { encodable in
- return try SQLiteDataEncoder().encode(encodable)
+ try SQLiteDataEncoder().encode(encodable)
}
} catch {
return self.eventLoop.makeFailedFuture(error)
}
- return self.database.query(
- serializer.sql,
- binds,
- logger: self.logger
- ) { row in
+
+ // This temporary silliness silences a Sendable capture warning whose correct resolution
+ // requires updating SQLKit itself to be fully Sendable-compliant.
+ @Sendable func onRowWorkaround(_ row: any SQLRow) {
onRow(row)
}
+ return self.database.query(serializer.sql, binds, logger: self.logger, onRowWorkaround)
}
}
+
diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift
index 5283528..8768b31 100644
--- a/Sources/SQLiteKit/SQLiteConnectionSource.swift
+++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift
@@ -5,7 +5,7 @@ import NIOPosix
import SQLiteNIO
import NIOCore
-public struct SQLiteConnectionSource: ConnectionPoolSource {
+public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable {
private let configuration: SQLiteConfiguration
private let actualURL: URL
private let threadPool: NIOThreadPool
diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift
index 51209ff..71cecde 100644
--- a/Sources/SQLiteKit/SQLiteDataDecoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift
@@ -1,10 +1,14 @@
import Foundation
import SQLiteNIO
+import NIOFoundationCompat
public struct SQLiteDataDecoder {
+ let json = JSONDecoder() // TODO: Add API to make this configurable
+
public init() {}
public func decode(_ type: T.Type, from data: SQLiteData) throws -> T {
+ // If `T` can be converted directly, just do so.
if let type = type as? any SQLiteDataConvertible.Type {
guard let value = type.init(sqliteData: data) else {
throw DecodingError.typeMismatch(T.self, .init(
@@ -14,54 +18,37 @@ public struct SQLiteDataDecoder {
}
return value as! T
} else {
- return try T.init(from: _Decoder(data: data))
- }
- }
-
- private final class _Decoder: Decoder {
- var codingPath: [any CodingKey] = []
- var userInfo: [CodingUserInfoKey: Any] = [:]
-
- let data: SQLiteData
- init(data: SQLiteData) { self.data = data }
-
- func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
- try self.jsonDecoder().unkeyedContainer()
- }
-
- func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer {
- try self.jsonDecoder().container(keyedBy: Key.self)
- }
-
- func jsonDecoder() throws -> any Decoder {
- let data: Data
- switch self.data {
- case .blob(let buffer): data = Data(buffer.readableBytesView)
- case .text(let string): data = Data(string.utf8)
- default: data = .init()
+ do {
+ return try T.init(from: GiftBoxUnwrapDecoder(decoder: self, data: data))
+ } catch is TryJSONSentinel {
+ // Couldn't unwrap it either. Fall back to attempting a JSON decode.
+ let buf: Data
+ switch data {
+ case .text(let str): buf = .init(str.utf8)
+ case .blob(let blob): buf = .init(buffer: blob, byteTransferStrategy: .noCopy)
+ // The remaining cases should never happen, but we implement them anyway just in case.
+ case .integer(let n): buf = .init(String(n).utf8)
+ case .float(let n): buf = .init(String(n).utf8)
+ case .null: buf = .init()
+ }
+ return try self.json.decode(T.self, from: buf)
}
- return try JSONDecoder().decode(DecoderUnwrapper.self, from: data).decoder
}
-
- func singleValueContainer() throws -> any SingleValueDecodingContainer { _SingleValueDecoder(self) }
}
+
+ private struct TryJSONSentinel: Swift.Error {}
- private struct _SingleValueDecoder: SingleValueDecodingContainer {
- var codingPath: [any CodingKey] { self.decoder.codingPath }
- let decoder: _Decoder
- init(_ decoder: _Decoder) { self.decoder = decoder }
-
- func decodeNil() -> Bool { self.decoder.data == .null }
-
- func decode(_: T.Type) throws -> T {
- try SQLiteDataDecoder().decode(T.self, from: self.decoder.data)
- }
- }
-}
-
-private struct DecoderUnwrapper: Decodable {
- let decoder: Decoder
- init(from decoder: Decoder) {
- self.decoder = decoder
+ private struct GiftBoxUnwrapDecoder: Decoder, SingleValueDecodingContainer {
+ let decoder: SQLiteDataDecoder
+ let data: SQLiteData
+
+ var codingPath: [any CodingKey] { [] }
+ var userInfo: [CodingUserInfoKey: Any] { [:] }
+
+ func container(keyedBy: K.Type) throws -> KeyedDecodingContainer { throw TryJSONSentinel() }
+ func unkeyedContainer() throws -> any UnkeyedDecodingContainer { throw TryJSONSentinel() }
+ func singleValueContainer() throws -> any SingleValueDecodingContainer { self }
+ func decodeNil() -> Bool { self.data.isNull }
+ func decode(_: T.Type) throws -> T { try self.decoder.decode(T.self, from: self.data) }
}
}
diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift
index bbf49b6..04ecfbb 100644
--- a/Sources/SQLiteKit/SQLiteDataEncoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift
@@ -9,16 +9,22 @@ public struct SQLiteDataEncoder {
if let data = (value as? any SQLiteDataConvertible)?.sqliteData {
return data
} else {
- let encoder = _Encoder()
+ let encoder = EncoderImpl()
+
try value.encode(to: encoder)
switch encoder.result {
case .data(let data):
return data
case .unkeyed, .keyed:
- let json = try JSONEncoder().encode(AnyEncodable(value))
- var buffer = ByteBufferAllocator().buffer(capacity: json.count)
- buffer.writeBytes(json)
- return SQLiteData.blob(buffer)
+ // Starting with SQLite 3.45.0 (2024-01-15), sending textual JSON as a blob will cause inexplicable
+ // errors due to the data being interpreted as JSONB (arguably not the best behavior for SQLite's API,
+ // but not technically a compatibility break). As there is no good way to get at the underlying SQLite
+ // version from the data encoder, and extending `SQLiteData` would make a rather epic mess, we now just
+ // always send JSON as text instead. This is technically what we should have been doing all along
+ // anyway, meaning this change is a bugfix. Good thing, too - otherwise we'd be stuck trying to retain
+ // bug-for-bug compatibility, starting with reverse-engineering SQLite's JSONB format (which is not the
+ // same as PostgreSQL's, of course).
+ return .text(.init(decoding: try JSONEncoder().encode(value), as: UTF8.self))
}
}
}
@@ -29,67 +35,49 @@ public struct SQLiteDataEncoder {
case data(SQLiteData)
}
- private final class _Encoder: Encoder {
- var codingPath: [any CodingKey] = []
- var userInfo: [CodingUserInfoKey: Any] = [:]
+ private final class EncoderImpl: Encoder, SingleValueEncodingContainer {
+ private struct KeyedEncoderImpl: KeyedEncodingContainerProtocol {
+ var codingPath: [any CodingKey] { [] }
+ mutating func encodeNil(forKey: K) throws {}
+ mutating func encode(_: some Encodable, forKey: K) throws {}
+ mutating func nestedContainer(keyedBy: N.Type, forKey: K) -> KeyedEncodingContainer { .init(KeyedEncoderImpl()) }
+ mutating func nestedUnkeyedContainer(forKey: K) -> any UnkeyedEncodingContainer { UnkeyedEncoderImpl() }
+ mutating func superEncoder() -> any Encoder { EncoderImpl() }
+ mutating func superEncoder(forKey: K) -> any Encoder { EncoderImpl() }
+ }
+
+ private struct UnkeyedEncoderImpl: UnkeyedEncodingContainer {
+ var codingPath: [any CodingKey] { [] }
+ var count: Int = 0
+ mutating func encodeNil() throws {}
+ mutating func encode(_: some Encodable) throws {}
+ mutating func nestedContainer(keyedBy: N.Type) -> KeyedEncodingContainer { .init(KeyedEncoderImpl()) }
+ mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { UnkeyedEncoderImpl() }
+ mutating func superEncoder() -> any Encoder { EncoderImpl() }
+ }
+
+ var codingPath: [any CodingKey] { [] }
+ var userInfo: [CodingUserInfoKey: Any] { [:] }
var result: Result
init() { self.result = .data(.null) }
- func container(keyedBy: Key.Type) -> KeyedEncodingContainer {
+ func container(keyedBy: K.Type) -> KeyedEncodingContainer {
self.result = .keyed
- return .init(_KeyedEncoder())
+ return .init(KeyedEncoderImpl())
}
func unkeyedContainer() -> any UnkeyedEncodingContainer {
self.result = .unkeyed
- return _UnkeyedEncoder()
+ return UnkeyedEncoderImpl()
}
- func singleValueContainer() -> any SingleValueEncodingContainer { _SingleValueEncoder(encoder: self) }
- }
-
- private struct _KeyedEncoder: KeyedEncodingContainerProtocol {
- var codingPath: [any CodingKey] = []
- mutating func encodeNil(forKey: Key) throws {}
- mutating func encode(_ value: some Encodable, forKey: Key) throws {}
- mutating func nestedContainer(keyedBy: Nested.Type, forKey: Key) -> KeyedEncodingContainer {
- .init(_KeyedEncoder())
- }
- mutating func nestedUnkeyedContainer(forKey: Key) -> any UnkeyedEncodingContainer { _UnkeyedEncoder() }
- mutating func superEncoder() -> any Encoder { _Encoder() }
- mutating func superEncoder(forKey: Key) -> any Encoder { _Encoder() }
- }
+ func singleValueContainer() -> any SingleValueEncodingContainer { self }
- private struct _UnkeyedEncoder: UnkeyedEncodingContainer {
- var codingPath: [any CodingKey] = []
- var count: Int = 0
- mutating func encodeNil() throws {}
- mutating func encode(_ value: some Encodable) throws {}
- mutating func nestedContainer(keyedBy: Nested.Type) -> KeyedEncodingContainer {
- .init(_KeyedEncoder())
- }
- mutating func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { _UnkeyedEncoder() }
- mutating func superEncoder() -> any Encoder { _Encoder() }
- }
-
- private struct _SingleValueEncoder: SingleValueEncodingContainer {
- var codingPath: [any CodingKey] { self.encoder.codingPath }
- let encoder: _Encoder
- mutating func encodeNil() throws { self.encoder.result = .data(.null) }
- mutating func encode(_ value: some Encodable) throws {
- let data = try SQLiteDataEncoder().encode(value)
- self.encoder.result = .data(data)
+ func encodeNil() throws { self.result = .data(.null) }
+
+ func encode(_ value: some Encodable) throws {
+ self.result = .data(try SQLiteDataEncoder().encode(value))
}
}
}
-
-private struct AnyEncodable: Encodable {
- let encodable: Encodable
- init(_ encodable: Encodable) {
- self.encodable = encodable
- }
- func encode(to encoder: Encoder) throws {
- try self.encodable.encode(to: encoder)
- }
-}
diff --git a/Tests/SQLiteKitTests/SQLiteKitTests.swift b/Tests/SQLiteKitTests/SQLiteKitTests.swift
index 600641a..c9cd241 100644
--- a/Tests/SQLiteKitTests/SQLiteKitTests.swift
+++ b/Tests/SQLiteKitTests/SQLiteKitTests.swift
@@ -8,6 +8,7 @@ import SQLKit
final class SQLiteKitTests: XCTestCase {
func testSQLKitBenchmark() throws {
let benchmark = SQLBenchmarker(on: self.db)
+
try benchmark.run()
}
@@ -18,15 +19,16 @@ final class SQLiteKitTests: XCTestCase {
try await self.db.drop(table: "galaxies")
.ifExists()
.run()
+
try await self.db.create(table: "galaxies")
.column("id", type: .int, .primaryKey)
.column("name", type: .text)
.run()
- try await self.db.create(table: "planets")
- .ifNotExists()
+ try await self.db.create(table: "planets").ifNotExists()
.column("id", type: .int, .primaryKey)
.column("galaxyID", type: .int, .references("galaxies", "id"))
.run()
+
try await self.db.alter(table: "planets")
.column("name", type: .text, .default(SQLLiteral.string("Unamed Planet")))
.run()
@@ -35,21 +37,22 @@ final class SQLiteKitTests: XCTestCase {
.column("id")
.unique()
.run()
+
// INSERT INTO "galaxies" ("id", "name") VALUES (DEFAULT, $1)
try await self.db.insert(into: "galaxies")
.columns("id", "name")
.values(SQLLiteral.null, SQLBind("Milky Way"))
.values(SQLLiteral.null, SQLBind("Andromeda"))
- // .value(Galaxy(name: "Milky Way"))
.run()
+
// SELECT * FROM galaxies WHERE name != NULL AND (name == ? OR name == ?)
_ = try await self.db.select()
.column("*")
.from("galaxies")
.where("name", .notEqual, SQLLiteral.null)
- .where {
- $0.where("name", .equal, SQLBind("Milky Way"))
- .orWhere("name", .equal, SQLBind("Andromeda"))
+ .where { $0
+ .orWhere("name", .equal, SQLBind("Milky Way"))
+ .orWhere("name", .equal, SQLBind("Andromeda"))
}
.all()
@@ -90,6 +93,7 @@ final class SQLiteKitTests: XCTestCase {
func testForeignKeysEnabledOnlyWhenRequested() async throws {
let res = try await self.connection.query("PRAGMA foreign_keys").get()
+
XCTAssertEqual(res[0].column("foreign_keys"), .integer(1))
// Using `.file` storage here is a quick and dirty nod to increasing test coverage.
@@ -97,10 +101,10 @@ final class SQLiteKitTests: XCTestCase {
configuration: .init(storage: .file(
path: FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID()).sqlite3", isDirectory: false).path
), enableForeignKeys: false),
- threadPool: self.threadPool
+ threadPool: .singleton
)
- let conn2 = try await source.makeConnection(logger: self.connection.logger, on: self.eventLoopGroup.any()).get()
+ let conn2 = try await source.makeConnection(logger: self.connection.logger, on: MultiThreadedEventLoopGroup.singleton.any()).get()
defer { try! conn2.close().wait() }
let res2 = try await conn2.query("PRAGMA foreign_keys").get()
@@ -122,20 +126,20 @@ final class SQLiteKitTests: XCTestCase {
func testMultipleInMemoryDatabases() async throws {
let a = SQLiteConnectionSource(
configuration: .init(storage: .memory, enableForeignKeys: true),
- threadPool: self.threadPool
+ threadPool: .singleton
)
let b = SQLiteConnectionSource(
configuration: .init(storage: .memory, enableForeignKeys: true),
- threadPool: self.threadPool
+ threadPool: .singleton
)
- let a1 = try await a.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).get()
+ let a1 = try await a.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get()
defer { try! a1.close().wait() }
- let a2 = try await a.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).get()
+ let a2 = try await a.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get()
defer { try! a2.close().wait() }
- let b1 = try await b.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).get()
+ let b1 = try await b.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get()
defer { try! b1.close().wait() }
- let b2 = try await b.makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).get()
+ let b2 = try await b.makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get()
defer { try! b2.close().wait() }
_ = try await a1.query("CREATE TABLE foo (bar INTEGER)").get()
@@ -192,20 +196,26 @@ final class SQLiteKitTests: XCTestCase {
let val: String
let nest: NestFoo
}
- try await self.db.create(table: "foo")
- .column("id", type: .int, .primaryKey(autoIncrement: false), .notNull)
- .column("value", type: .custom(SQLRaw("json")))
- .run()
- try await self.db.insert(into: "foo")
- .columns("id", "value")
- .values(SQLLiteral.numeric("1"), SQLBind(SubFoo(arr: [1,2,3], val: "a", nest: .init(x: 1.1))))
- .values(SQLLiteral.numeric("2"), SQLBind(SubFoo?.none))
- .run()
- let rows = try await self.db.select()
- .column(self.db.dialect.nestedSubpathExpression(in: SQLColumn("value"), for: ["nest", "x"])!, as: "x")
- .from("foo")
- .orderBy("id")
- .all()
+ await XCTAssertNoThrowAsync(
+ try await self.db.create(table: "foo")
+ .column("id", type: .int, .primaryKey(autoIncrement: false), .notNull)
+ .column("value", type: .custom(SQLRaw("json")))
+ .run()
+ )
+ await XCTAssertNoThrowAsync(
+ try await self.db.insert(into: "foo")
+ .columns("id", "value")
+ .values(SQLLiteral.numeric("1"), SQLBind(SubFoo(arr: [1,2,3], val: "a", nest: .init(x: 1.1))))
+ .values(SQLLiteral.numeric("2"), SQLBind(SubFoo?.none))
+ .run()
+ )
+ let rows = try await XCTUnwrapAsync(
+ try await self.db.select()
+ .column(self.db.dialect.nestedSubpathExpression(in: SQLColumn("value"), for: ["nest", "x"])!, as: "x")
+ .from("foo")
+ .orderBy("id")
+ .all()
+ )
XCTAssertEqual(rows.count, 2)
let row1 = try XCTUnwrap(rows.dropFirst(0).first),
@@ -226,30 +236,23 @@ final class SQLiteKitTests: XCTestCase {
}
var db: any SQLDatabase { self.connection.sql() }
- var benchmark: SQLBenchmarker { .init(on: self.db) }
-
- var eventLoopGroup: (any EventLoopGroup)!
- var threadPool: NIOThreadPool!
var connection: SQLiteConnection!
override func setUp() async throws {
XCTAssertTrue(isLoggingConfigured)
- self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)
- self.threadPool = NIOThreadPool(numberOfThreads: 2)
- self.threadPool.start()
+
self.connection = try await SQLiteConnectionSource(
configuration: .init(storage: .memory, enableForeignKeys: true),
- threadPool: self.threadPool
- ).makeConnection(logger: .init(label: "test"), on: self.eventLoopGroup.any()).get()
+ threadPool: .singleton
+ ).makeConnection(
+ logger: .init(label: "test"),
+ on: MultiThreadedEventLoopGroup.singleton.any()
+ ).get()
}
override func tearDown() async throws {
try await self.connection.close().get()
self.connection = nil
- try await self.threadPool.shutdownGracefully()
- self.threadPool = nil
- try await self.eventLoopGroup.shutdownGracefully()
- self.eventLoopGroup = nil
}
}
@@ -260,8 +263,35 @@ func env(_ name: String) -> String? {
let isLoggingConfigured: Bool = {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
- handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .debug
+ handler.logLevel = env("LOG_LEVEL").flatMap { .init(rawValue: $0) } ?? .debug
return handler
}
return true
}()
+
+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)
+ }
+}
+
+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)
+}