From 11edd9bcdef074a088b5d6c028d0d97fba03c2e2 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 00:48:32 -0600
Subject: [PATCH 1/9] General package cleanup
---
Package.swift | 4 +-
Package@swift-5.9.swift | 44 +++++++++++++
README.md | 24 +++++---
.../SQLiteKit/Docs.docc/images/article.svg | 1 -
.../Docs.docc/images/vapor-sqlite-logo.svg | 58 ------------------
.../Docs.docc/images/vapor-sqlitekit-logo.svg | 22 +++++++
.../SQLiteKit/Docs.docc/theme-settings.json | 61 ++++++-------------
7 files changed, 100 insertions(+), 114 deletions(-)
create mode 100644 Package@swift-5.9.swift
delete mode 100644 Sources/SQLiteKit/Docs.docc/images/article.svg
delete mode 100644 Sources/SQLiteKit/Docs.docc/images/vapor-sqlite-logo.svg
create mode 100644 Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg
diff --git a/Package.swift b/Package.swift
index d1ccfb3..31f4c51 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,9 +13,9 @@ 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/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: [
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..bc79d0c
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,44 @@
+// 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/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: "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..b1797bc 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,18 @@
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
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 }
+ }
}
From 411250a18bef20b8a5e56f2798dc1ba338649523 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 00:49:48 -0600
Subject: [PATCH 2/9] Send JSON to SQLite as TEXT, not BLOB. This restores
compatibility with SQLite 3.45.0 and above, due to the new JSONB support
changing the interpretation of JSON data in BLOB form (and is what should've
been happening all along regardless).
---
Sources/SQLiteKit/SQLiteDataEncoder.swift | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift
index bbf49b6..d398ca0 100644
--- a/Sources/SQLiteKit/SQLiteDataEncoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift
@@ -15,10 +15,15 @@ public struct SQLiteDataEncoder {
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 SQLiteData.text(.init(decoding: try JSONEncoder().encode(value), as: UTF8.self))
}
}
}
From 29a90bb005dbf7e660c701311dffeaa1814fa073 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 00:49:59 -0600
Subject: [PATCH 3/9] Some tests cleanup
---
Tests/SQLiteKitTests/SQLiteKitTests.swift | 90 ++++++++++++++---------
1 file changed, 57 insertions(+), 33 deletions(-)
diff --git a/Tests/SQLiteKitTests/SQLiteKitTests.swift b/Tests/SQLiteKitTests/SQLiteKitTests.swift
index 600641a..bcc5304 100644
--- a/Tests/SQLiteKitTests/SQLiteKitTests.swift
+++ b/Tests/SQLiteKitTests/SQLiteKitTests.swift
@@ -97,10 +97,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 +122,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 +192,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),
@@ -228,28 +234,19 @@ 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
}
}
@@ -265,3 +262,30 @@ let isLoggingConfigured: Bool = {
}
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)
+}
From 434a6f2ab245dfdee47790e2644153f44916034f Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 01:01:27 -0600
Subject: [PATCH 4/9] Add AnyExistential correctness.
---
Sources/SQLiteKit/SQLiteConnection+SQLKit.swift | 2 +-
Sources/SQLiteKit/SQLiteDataDecoder.swift | 4 ++--
Sources/SQLiteKit/SQLiteDataEncoder.swift | 10 ----------
3 files changed, 3 insertions(+), 13 deletions(-)
diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
index 3c2cd85..9ccb36c 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
diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift
index 51209ff..b754464 100644
--- a/Sources/SQLiteKit/SQLiteDataDecoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift
@@ -60,8 +60,8 @@ public struct SQLiteDataDecoder {
}
private struct DecoderUnwrapper: Decodable {
- let decoder: Decoder
- init(from decoder: Decoder) {
+ let decoder: any Decoder
+ init(from decoder: any Decoder) {
self.decoder = decoder
}
}
diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift
index d398ca0..1164e01 100644
--- a/Sources/SQLiteKit/SQLiteDataEncoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift
@@ -88,13 +88,3 @@ public struct SQLiteDataEncoder {
}
}
}
-
-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)
- }
-}
From 11aa43f5003a22dba9bdb9d8b9cd21d9a94d0b4d Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 01:28:46 -0600
Subject: [PATCH 5/9] Quick and dirty fix for yet another of Tanner's
disastrous misuses of Codable.
---
Sources/SQLiteKit/SQLiteDataDecoder.swift | 35 ++++++++++-------------
1 file changed, 15 insertions(+), 20 deletions(-)
diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift
index b754464..3fca028 100644
--- a/Sources/SQLiteKit/SQLiteDataDecoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift
@@ -14,9 +14,21 @@ public struct SQLiteDataDecoder {
}
return value as! T
} else {
- return try T.init(from: _Decoder(data: data))
+ do {
+ return try T.init(from: _Decoder(data: data))
+ } catch is SentinelError {
+ let fdata: Data
+ switch data {
+ case .blob(let buf): fdata = .init(buf.readableBytesView)
+ case .text(let str): fdata = .init(str.utf8)
+ default: fdata = .init()
+ }
+ return try JSONDecoder().decode(T.self, from: fdata)
+ }
}
}
+
+ private struct SentinelError: Swift.Error {}
private final class _Decoder: Decoder {
var codingPath: [any CodingKey] = []
@@ -26,21 +38,11 @@ public struct SQLiteDataDecoder {
init(data: SQLiteData) { self.data = data }
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
- try self.jsonDecoder().unkeyedContainer()
+ throw SentinelError()
}
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()
- }
- return try JSONDecoder().decode(DecoderUnwrapper.self, from: data).decoder
+ throw SentinelError()
}
func singleValueContainer() throws -> any SingleValueDecodingContainer { _SingleValueDecoder(self) }
@@ -58,10 +60,3 @@ public struct SQLiteDataDecoder {
}
}
}
-
-private struct DecoderUnwrapper: Decodable {
- let decoder: any Decoder
- init(from decoder: any Decoder) {
- self.decoder = decoder
- }
-}
From 73b5097587f595d4d2804fb2f70c93c5ef6af21d Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 04:54:55 -0600
Subject: [PATCH 6/9] Add more Sendable compliance
---
Sources/SQLiteKit/SQLiteConfiguration.swift | 4 ++--
Sources/SQLiteKit/SQLiteConnection+SQLKit.swift | 13 +++++++------
Sources/SQLiteKit/SQLiteConnectionSource.swift | 2 +-
3 files changed, 10 insertions(+), 9 deletions(-)
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 9ccb36c..bde2384 100644
--- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
+++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
@@ -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
From 9a680bb8b3ce6b938bced7a9dce5e714e0ec0668 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 04:55:23 -0600
Subject: [PATCH 7/9] Clean up encoder and decoder logic.
---
Package.swift | 2 +
Package@swift-5.9.swift | 2 +
Sources/SQLiteKit/SQLiteDataDecoder.swift | 62 ++++++++-----------
Sources/SQLiteKit/SQLiteDataEncoder.swift | 75 ++++++++++-------------
4 files changed, 65 insertions(+), 76 deletions(-)
diff --git a/Package.swift b/Package.swift
index 31f4c51..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/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"),
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
index bc79d0c..43bd242 100644
--- a/Package@swift-5.9.swift
+++ b/Package@swift-5.9.swift
@@ -18,6 +18,7 @@ let package = Package(
.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"),
@@ -26,6 +27,7 @@ let package = Package(
.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/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift
index 3fca028..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(
@@ -15,48 +19,36 @@ public struct SQLiteDataDecoder {
return value as! T
} else {
do {
- return try T.init(from: _Decoder(data: data))
- } catch is SentinelError {
- let fdata: Data
+ 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 .blob(let buf): fdata = .init(buf.readableBytesView)
- case .text(let str): fdata = .init(str.utf8)
- default: fdata = .init()
+ 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 JSONDecoder().decode(T.self, from: fdata)
+ return try self.json.decode(T.self, from: buf)
}
}
}
- private struct SentinelError: Swift.Error {}
-
- private final class _Decoder: Decoder {
- var codingPath: [any CodingKey] = []
- var userInfo: [CodingUserInfoKey: Any] = [:]
+ private struct TryJSONSentinel: Swift.Error {}
+ private struct GiftBoxUnwrapDecoder: Decoder, SingleValueDecodingContainer {
+ let decoder: SQLiteDataDecoder
let data: SQLiteData
- init(data: SQLiteData) { self.data = data }
-
- func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
- throw SentinelError()
- }
-
- func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer {
- throw SentinelError()
- }
-
- func singleValueContainer() throws -> any SingleValueDecodingContainer { _SingleValueDecoder(self) }
- }
-
- 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)
- }
+
+ 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 1164e01..04ecfbb 100644
--- a/Sources/SQLiteKit/SQLiteDataEncoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift
@@ -9,7 +9,8 @@ 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):
@@ -23,7 +24,7 @@ public struct SQLiteDataEncoder {
// 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 SQLiteData.text(.init(decoding: try JSONEncoder().encode(value), as: UTF8.self))
+ return .text(.init(decoding: try JSONEncoder().encode(value), as: UTF8.self))
}
}
}
@@ -34,57 +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) }
- }
+ func singleValueContainer() -> any SingleValueEncodingContainer { 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() }
- }
-
- 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))
}
}
}
From 2063a0bb579f1c3c4e7296e835e0bb18f43a92c2 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 04:55:30 -0600
Subject: [PATCH 8/9] Add minimum content to README
---
README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/README.md b/README.md
index b1797bc..9102bb7 100644
--- a/README.md
+++ b/README.md
@@ -16,3 +16,13 @@
+
+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
From 57344143ad06099d0422733b6f1f03db11f51ae3 Mon Sep 17 00:00:00 2001
From: Gwynne Raskind
Date: Thu, 18 Jan 2024 04:56:04 -0600
Subject: [PATCH 9/9] A bit more test cleanup
---
Tests/SQLiteKitTests/SQLiteKitTests.swift | 26 ++++++++++++++---------
1 file changed, 16 insertions(+), 10 deletions(-)
diff --git a/Tests/SQLiteKitTests/SQLiteKitTests.swift b/Tests/SQLiteKitTests/SQLiteKitTests.swift
index bcc5304..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.
@@ -232,16 +236,18 @@ final class SQLiteKitTests: XCTestCase {
}
var db: any SQLDatabase { self.connection.sql() }
- var benchmark: SQLBenchmarker { .init(on: self.db) }
-
var connection: SQLiteConnection!
override func setUp() async throws {
XCTAssertTrue(isLoggingConfigured)
+
self.connection = try await SQLiteConnectionSource(
configuration: .init(storage: .memory, enableForeignKeys: true),
threadPool: .singleton
- ).makeConnection(logger: .init(label: "test"), on: MultiThreadedEventLoopGroup.singleton.any()).get()
+ ).makeConnection(
+ logger: .init(label: "test"),
+ on: MultiThreadedEventLoopGroup.singleton.any()
+ ).get()
}
override func tearDown() async throws {
@@ -257,7 +263,7 @@ 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