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 - + + + + SQLiteKit +

-SSWG Incubation -Documentation -MIT License -Continuous Integration -Swift 5.7 +Documentation +Team Chat +MIT License +Continuous Integration + +Swift 5.7+ +SSWG Incubation Level: Graduated

+ +
+ +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 @@ - - - SQLiteNIO - - - - - - - - - - - - - - - - - - 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) +}