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 @@

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

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