From bef7b6d176fd827474fb9992891196c6ba300548 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 1 May 2024 07:09:42 -0500 Subject: [PATCH 01/11] Update package structure, bump Swift minimum to 5.8, require an SQLKit we actually need, update CI, fix ExistentialAny usage, fix readme, update docs and logo --- .github/CODEOWNERS | 9 ++++- .github/dependabot.yml | 12 ------ .github/workflows/test.yml | 3 +- Package.swift | 39 +++++++++++++------ Package@swift-5.9.swift | 15 ++++--- README.md | 37 +++++++++++------- Sources/SQLiteKit/Docs.docc/Documentation.md | 9 ++++- .../Docs.docc/images/vapor-sqlitekit-logo.svg | 27 +++++++------ .../SQLiteKit/Docs.docc/theme-settings.json | 8 ++-- Sources/SQLiteKit/Exports.swift | 11 ------ 10 files changed, 94 insertions(+), 76 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c99cc74..6ff9614 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,8 @@ -* @0xTim @gwynne +* @gwynne +/.github/CONTRIBUTING.md @gwynne @0xTim +/.github/workflows/*.yml @gwynne @0xTim +/.github/workflows/test.yml @gwynne +/.spi.yml @gwynne @0xTim +/.gitignore @gwynne @0xTim +/LICENSE @gwynne @0xTim +/README.md @gwynne @0xTim diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 68cfb1a..998a0eb 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,4 @@ version: 2 -enable-beta-ecosystems: true updates: - package-ecosystem: "github-actions" directory: "/" @@ -9,14 +8,3 @@ updates: dependencies: patterns: - "*" - - package-ecosystem: "swift" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 6 - allow: - - dependency-type: all - groups: - all-dependencies: - patterns: - - "*" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2423c1d..f15c073 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,12 +15,13 @@ jobs: # also serves as code coverage baseline update unit-tests: uses: vapor/ci/.github/workflows/run-unit-tests.yml@main + secrets: inherit # Make sure downstream dependents still work dependents-check: if: ${{ !(github.event.pull_request.draft || false) }} runs-on: ubuntu-latest - container: swift:5.9-jammy + container: swift:5.10-jammy steps: - name: Check out package uses: actions/checkout@v4 diff --git a/Package.swift b/Package.swift index 7ef451d..2ec2375 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 import PackageDescription let package = Package( @@ -15,19 +15,34 @@ let package = Package( 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/sql-kit.git", from: "3.29.3"), .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"), - ]), - .testTarget(name: "SQLiteKitTests", dependencies: [ - .product(name: "SQLKitBenchmark", package: "sql-kit"), - .target(name: "SQLiteKit"), - ]), + .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 + ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 43bd242..f5f9fe7 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -1,11 +1,6 @@ // swift-tools-version:5.9 import PackageDescription -let swiftSettings: [SwiftSetting] = [ - .enableUpcomingFeature("ExistentialAny"), - .enableExperimentalFeature("StrictConcurrency=complete"), -] - let package = Package( name: "sqlite-kit", platforms: [ @@ -20,7 +15,7 @@ let package = Package( 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/sql-kit.git", from: "3.29.3"), .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), ], targets: [ @@ -44,3 +39,11 @@ let package = Package( ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/README.md b/README.md index 9102bb7..86287bc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- - - SQLiteKit + + + SQLiteKit

@@ -10,19 +10,30 @@ Team Chat MIT License Continuous Integration - -Swift 5.7+ -SSWG Incubation Level: Graduated + +Swift 5.8+ +SSWG Incubation Level: Graduated


-SQLiteKit is a library providing an [SQLKit] driver for [SQLiteNIO]. +SQLiteKit is an [SQLKit] driver for SQLite clients. It supports building and serializing SQLite-dialect SQL queries. SQLiteKit uses [SQLiteNIO] to connect and communicate with the database server asynchronously. [AsyncKit] is used to provide connection pooling. -> [!NOTE] -> The [FluentKit] driver for SQLite is provided by the [FluentSQLiteDriver] package. +[SQLKit]: https://github.com/vapor/sql-kit +[SQLiteNIO]: https://github.com/vapor/sqlite-nio +[AsyncKit]: https://github.com/vapor/async-kit -[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 +### Usage + +Use the SPM string to easily include the dependendency in your `Package.swift` file. + +```swift +.package(url: "https://github.com/vapor/sqlite-kit.git", from: "4.0.0") +``` + +### Supported Platforms + +SQLiteKit supports the following platforms: + +- Ubuntu 20.04+ +- macOS 10.15+ diff --git a/Sources/SQLiteKit/Docs.docc/Documentation.md b/Sources/SQLiteKit/Docs.docc/Documentation.md index cbf97f2..84d3fea 100644 --- a/Sources/SQLiteKit/Docs.docc/Documentation.md +++ b/Sources/SQLiteKit/Docs.docc/Documentation.md @@ -12,11 +12,16 @@ This package provides the "foundational" level of support for using [Fluent] wit - Managing the underlying SQLite library ([SQLiteNIO]), - Providing a two-way bridge between SQLiteNIO and SQLKit's generic data and metadata formats, -- Presenting an interface for establishing, managing, and interacting with database connections. +- Presenting an interface for establishing, managing, and interacting with database connections via [AsyncKit]. -> Note: The FluentKit driver for SQLite is provided by the [FluentSQLiteDriver] package. +> Tip: The FluentKit driver for SQLite is provided by the [FluentSQLiteDriver] package. + +## Version Support + +This package uses [SQLiteNIO] for all underlying database interactions. The version of SQLite embedded by that package is always the one used. [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 +[AsyncKit]: https://swiftpackageindex.com/vapor/async-kit diff --git a/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg b/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg index a3d3287..fc638a3 100644 --- a/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg +++ b/Sources/SQLiteKit/Docs.docc/images/vapor-sqlitekit-logo.svg @@ -1,22 +1,21 @@ - + - - - + + + - - + + - - + + - - - + + + \ No newline at end of file diff --git a/Sources/SQLiteKit/Docs.docc/theme-settings.json b/Sources/SQLiteKit/Docs.docc/theme-settings.json index 806ccb9..bc7e116 100644 --- a/Sources/SQLiteKit/Docs.docc/theme-settings.json +++ b/Sources/SQLiteKit/Docs.docc/theme-settings.json @@ -1,13 +1,13 @@ { "theme": { - "aside": { "border-radius": "6px", "border-style": "double", "border-width": "3px" }, + "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, "border-radius": "0", "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, "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)", + "sqlitekit": "hsl(215, 45%, 58%)", + "documentation-intro-fill": "radial-gradient(circle at top, var(--color-sqlitekit) 30%, #000 100%)", + "documentation-intro-accent": "var(--color-sqlitekit)", "logo-base": { "dark": "#fff", "light": "#000" }, "logo-shape": { "dark": "#000", "light": "#fff" }, "fill": { "dark": "#000", "light": "#fff" } diff --git a/Sources/SQLiteKit/Exports.swift b/Sources/SQLiteKit/Exports.swift index 2719894..6656d7a 100644 --- a/Sources/SQLiteKit/Exports.swift +++ b/Sources/SQLiteKit/Exports.swift @@ -1,15 +1,4 @@ -#if swift(>=5.8) - @_documentation(visibility: internal) @_exported import SQLKit @_documentation(visibility: internal) @_exported import SQLiteNIO @_documentation(visibility: internal) @_exported import AsyncKit @_documentation(visibility: internal) @_exported import struct Logging.Logger - -#else - -@_exported import SQLKit -@_exported import SQLiteNIO -@_exported import AsyncKit -@_exported import struct Logging.Logger - -#endif From 044fa3dd2f1b111991c3e064a703604ccf2a515e Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 1 May 2024 07:16:07 -0500 Subject: [PATCH 02/11] Sendable correctness, de-underscore the SQLite version type, add support for specifying JSON encoders and decoders, add support for new SQLKit functionality, default to singleton NIOThreadPool, modernize data encoder and decoder to handle data better and faster --- .../SQLiteKit/SQLiteConnection+SQLKit.swift | 167 +++++++++++----- .../SQLiteKit/SQLiteConnectionSource.swift | 6 +- Sources/SQLiteKit/SQLiteDataDecoder.swift | 77 ++++++-- Sources/SQLiteKit/SQLiteDataEncoder.swift | 180 +++++++++++++----- Sources/SQLiteKit/SQLiteDialect.swift | 2 +- Sources/SQLiteKit/SQLiteRow+SQLRow.swift | 55 ++++-- 6 files changed, 350 insertions(+), 137 deletions(-) diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift index bde2384..c570648 100644 --- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift +++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift @@ -2,38 +2,57 @@ import SQLKit import SQLiteNIO extension SQLiteDatabase { - public func sql() -> any SQLDatabase { - _SQLiteSQLDatabase(database: self) + /// Return an object allowing access to this database via the `SQLDatabase` interface. + /// + /// - Parameters: + /// - encoder: An ``SQLiteDataEncoder`` used to translate bound query parameters into `SQLiteData` values. + /// - decoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. + /// - Returns: An instance of `SQLDatabase` which accesses the same database as `self`. + public func sql( + encoder: SQLiteDataEncoder = .init(), + decoder: SQLiteDataDecoder = .init() + ) -> any SQLDatabase { + SQLiteSQLDatabase(database: .init(value: self), encoder: encoder, decoder: decoder) } } -internal struct _SQLiteDatabaseVersion: SQLDatabaseReportedVersion { - /// The numeric value of the version. The format of the value is the one described in - /// https://sqlite.org/c3ref/c_source_id.html for the `SQLITE_VERSION_NUMBER` constant. +struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { + /// The numeric value of the version. + /// + /// The value is laid out identicallly to [the `SQLITE_VERSION_NUMBER` constant](c_source_id). + /// + /// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html let intValue: Int - /// The string representation of the version. The string is formatted according to the description in - /// https://sqlite.org/c3ref/c_source_id.html for the `SQLITE_VERSION` constant. + /// The string representation of the version. + /// + /// The string is formatted identically to [the `SQLITE_VERSION` constant](c_source_id). /// - /// This value is not used for equality or ordering comparisons; it is really only useful as a display value. We - /// maintain a stored property for it here rather than always generating it as-needed from the numeric value so - /// that we don't accidentally drop any additional information a particular library version might contain. + /// [c_source_id]: https://sqlite.org/c3ref/c_source_id.html /// - /// - Note: The string value should always represent the same version as the numeric value. This requirement is - /// asserted in debug builds, but not otherwise enforced. + /// This value is not used for equality or ordering comparisons; it is really only useful for display. We + /// maintain a stored property for it rather than generating it as-needed from the numeric value in order to + /// preserve any additional information the original value may contain. + /// + /// > Note: The string value should always represent the same version as the numeric value. This requirement is + /// > asserted in debug builds, but is not otherwise enforced. let stringValue: String - /// Separates a numeric value into individual components and returns them. + /// Separates an appropriately formatted numeric value into its individual components. static func components(of intValue: Int) -> (major: Int, minor: Int, patch: Int) { - let major = intValue / 1_000_000, - minor = (intValue - major * 1_000_000) / 1_000, - patch = intValue - major * 1_000_000 - minor * 1_000 - return (major: major, minor: minor, patch: patch) + ( + major: intValue / 1_000_000, + minor: intValue % 1_000_000 / 1_000, + patch: intValue % 1_000 + ) } - /// Get the version value representing the runtime version of the SQLite3 library in use. - static var runtimeVersion: _SQLiteDatabaseVersion { - self.init(intValue: Int(SQLiteConnection.libraryVersion()), stringValue: SQLiteConnection.libraryVersionString()) + /// Get the runtime version of the SQLite3 library in use. + static var runtimeVersion: Self { + self.init( + intValue: Int(SQLiteConnection.libraryVersion()), + stringValue: SQLiteConnection.libraryVersionString() + ) } /// Build a version value from individual components and synthesize the approiate string value. @@ -47,80 +66,130 @@ internal struct _SQLiteDatabaseVersion: SQLDatabaseReportedVersion { let components = Self.components(of: intValue) self.intValue = intValue - if let stringValue = stringValue { - assert(stringValue.hasPrefix("\(components.major).\(components.minor).\(components.patch)"), "SQLite version string '\(stringValue)' must match numeric version '\(intValue)'") + if let stringValue { + assert( + stringValue.hasPrefix("\(components.major).\(components.minor).\(components.patch)"), + "SQLite version string '\(stringValue)' must prefix-match numeric version '\(intValue)'" + ) self.stringValue = stringValue } else { self.stringValue = "\(components.major).\(components.major).\(components.patch)" } } - /// The major version number. This is likely to be 3 for a long time to come yet. - var majorVersion: Int { Self.components(of: self.intValue).major } + /// The major version number. + /// + /// This is likely to be 3 for a long time to come yet. + var majorVersion: Int { + Self.components(of: self.intValue).major + } /// The minor version number. - var minorVersion: Int { Self.components(of: self.intValue).minor } + var minorVersion: Int { + Self.components(of: self.intValue).minor + } /// The patch version number. - var patchVersion: Int { Self.components(of: self.intValue).patch } + var patchVersion: Int { + Self.components(of: self.intValue).patch + } - /// See ``SQLDatabaseReportedVersion/isEqual(to:)``. + // See `SQLDatabaseReportedVersion.isEqual(to:)`. func isEqual(to otherVersion: any SQLDatabaseReportedVersion) -> Bool { - (otherVersion as? _SQLiteDatabaseVersion).map { $0.intValue == self.intValue } ?? false + (otherVersion as? Self).map { $0.intValue == self.intValue } ?? false } - /// See ``SQLDatabaseReportedVersion/isOlder(than:)``. + // See `SQLDatabaseReportedVersion.isOlder(than:)`. func isOlder(than otherVersion: any SQLDatabaseReportedVersion) -> Bool { - (otherVersion as? _SQLiteDatabaseVersion).map { - (self.majorVersion < $0.majorVersion ? true : - (self.majorVersion > $0.majorVersion ? false : - (self.minorVersion < $0.minorVersion ? true : - (self.minorVersion > $0.minorVersion ? false : - (self.patchVersion < $0.patchVersion ? true : false))))) + (otherVersion as? Self).map { + (self.majorVersion != $0.majorVersion ? self.majorVersion < $0.majorVersion : + (self.minorVersion != $0.minorVersion ? self.minorVersion < $0.minorVersion : + (self.patchVersion < $0.patchVersion))) } ?? false } } -private struct _SQLiteSQLDatabase: SQLDatabase { - let database: any SQLiteDatabase +/// Wraps a `SQLiteDatabase` with the `SQLDatabase` protocol. +private struct SQLiteSQLDatabase: SQLDatabase { + /// A trivial wrapper type to work around Sendable warnings due to SQLiteNIO not being Sendable-correct. + struct FakeSendable: @unchecked Sendable { + let value: T + } + /// The underlying database. + let database: FakeSendable + + /// An ``SQLiteDataEncoder`` used to translate bindings into `SQLiteData` values. + let encoder: SQLiteDataEncoder + + /// An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. + let decoder: SQLiteDataDecoder var eventLoop: any EventLoop { - self.database.eventLoop + self.database.value.eventLoop } var version: (any SQLDatabaseReportedVersion)? { - _SQLiteDatabaseVersion.runtimeVersion + SQLiteDatabaseVersion.runtimeVersion } var logger: Logger { - self.database.logger + self.database.value.logger } var dialect: any SQLDialect { SQLiteDialect() } + // See `SQLDatabase.queryLogLevel`. + var queryLogLevel: Logger.Level? + func execute( sql query: any SQLExpression, - _ onRow: @escaping (any SQLRow) -> () + _ onRow: @escaping @Sendable (any SQLRow) -> () ) -> EventLoopFuture { var serializer = SQLSerializer(database: self) query.serialize(to: &serializer) let binds: [SQLiteData] do { binds = try serializer.binds.map { encodable in - try SQLiteDataEncoder().encode(encodable) + try self.encoder.encode(encodable) } } catch { return self.eventLoop.makeFailedFuture(error) } - // 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) + return self.database.value.query( + serializer.sql, + binds, + logger: self.logger, + { onRow($0.sql(decoder: self.decoder)) } + ) } -} + // See `SQLDatabase.execute(sql:_:)`. + func execute( + sql query: any SQLExpression, + _ onRow: @escaping @Sendable (any SQLRow) -> () + ) async throws { + var serializer = SQLSerializer(database: self) + query.serialize(to: &serializer) + + let binds = try serializer.binds.map { try self.encoder.encode($0) } + + return try await self.database.value.query( + serializer.sql, + binds, + logger: self.logger, + { onRow($0.sql(decoder: self.decoder)) } + ).get() + } + + // See `SQLDatabase.withSession(_:)`. + func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { + try await self.database.value.withConnection { c in + c.eventLoop.makeFutureWithTask { + try await closure(c.sql(encoder: self.encoder, decoder: self.decoder)) + } + }.get() + } +} diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift index 8768b31..ac40710 100644 --- a/Sources/SQLiteKit/SQLiteConnectionSource.swift +++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift @@ -16,7 +16,7 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { public init( configuration: SQLiteConfiguration, - threadPool: NIOThreadPool + threadPool: NIOThreadPool = .singleton ) { self.configuration = configuration self.actualURL = configuration.storage.urlForSQLite @@ -27,7 +27,7 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { logger: Logger, on eventLoop: any EventLoop ) -> EventLoopFuture { - return SQLiteConnection.open( + SQLiteConnection.open( storage: self.connectionStorage, threadPool: self.threadPool, logger: logger, @@ -43,7 +43,7 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { } } -extension SQLiteConnection: ConnectionPoolItem { } +extension SQLiteNIO.SQLiteConnection: AsyncKit.ConnectionPoolItem {} fileprivate extension String { var asSafeFilename: String { diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift index 71cecde..115b8d4 100644 --- a/Sources/SQLiteKit/SQLiteDataDecoder.swift +++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift @@ -1,28 +1,40 @@ import Foundation import SQLiteNIO +@_spi(CodableUtilities) import SQLKit import NIOFoundationCompat -public struct SQLiteDataDecoder { - let json = JSONDecoder() // TODO: Add API to make this configurable +public struct SQLiteDataDecoder: Sendable { + /// A wrapper to silence `Sendable` warnings for `JSONDecoder` when not on macOS. + struct FakeSendable: @unchecked Sendable { let value: T } - public init() {} + /// The `JSONDecoder` used for decoding values that can't be directly converted. + let json: FakeSendable + /// Initialize a ``SQLiteDataDecoder`` with a JSON decoder. + /// + /// - Parameter json: A `JSONDecoder` to use for decoding types that can't be directly converted. Defaults + /// to an unconfigured decoder. + public init(json: JSONDecoder = .init()) { + self.json = .init(value: json) + } + 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( codingPath: [], - debugDescription: "Could not initialize \(T.self) from \(data)." + debugDescription: "Could not convert SQLite data to \(T.self): \(data)." )) } return value as! T } else { do { - return try T.init(from: GiftBoxUnwrapDecoder(decoder: self, data: data)) - } catch is TryJSONSentinel { + return try T.init(from: NestedSingleValueUnwrappingDecoder(decoder: self, data: data)) + } catch is SQLCodingError { // 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) @@ -31,24 +43,55 @@ public struct SQLiteDataDecoder { case .float(let n): buf = .init(String(n).utf8) case .null: buf = .init() } - return try self.json.decode(T.self, from: buf) + return try self.json.value.decode(T.self, from: buf) } } } - private struct TryJSONSentinel: Swift.Error {} + /// A trivial decoder for unwrapping types which decode as trivial single-value containers. This allows for + /// correct handling of types such as `Optional` when they do not conform to `SQLiteDataCovnertible`. + private final class NestedSingleValueUnwrappingDecoder: Decoder, SingleValueDecodingContainer { + // See `Decoder.codingPath` and `SingleValueDecodingContainer.codingPath`. + var codingPath: [any CodingKey] { [] } + + // See `Decoder.userInfo`. + var userInfo: [CodingUserInfoKey: Any] { [:] } - private struct GiftBoxUnwrapDecoder: Decoder, SingleValueDecodingContainer { - let decoder: SQLiteDataDecoder + /// The parent ``SQLiteDataDecoder``. + let dataDecoder: SQLiteDataDecoder + + /// The data to decode. let data: SQLiteData - var codingPath: [any CodingKey] { [] } - var userInfo: [CodingUserInfoKey: Any] { [:] } + /// Create a new decoder with an ``SQLiteDataDecoder`` and the data to decode. + init(decoder: SQLiteDataDecoder, data: SQLiteData) { + self.dataDecoder = decoder + self.data = data + } + + // See `Decoder.container(keyedBy:)`. + func container(keyedBy: Key.Type) throws -> KeyedDecodingContainer { + throw .invalid(at: self.codingPath) + } + + // See `Decoder.unkeyedContainer()`. + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { + throw .invalid(at: self.codingPath) + } + + // See `Decoder.singleValueContainer()`. + func singleValueContainer() throws -> any SingleValueDecodingContainer { + self + } + + // See `SingleValueDecodingContainer.decodeNil()`. + func decodeNil() -> Bool { + self.data.isNull + } - 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) } + // See `SingleValueDecodingContainer.decode(_:)`. + func decode(_: T.Type) throws -> T { + try self.dataDecoder.decode(T.self, from: self.data) + } } } diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift index 04ecfbb..613e0a0 100644 --- a/Sources/SQLiteKit/SQLiteDataEncoder.swift +++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift @@ -1,21 +1,32 @@ import NIOCore import Foundation +@_spi(CodableUtilities) import SQLKit import SQLiteNIO -public struct SQLiteDataEncoder { - public init() {} - +public struct SQLiteDataEncoder: Sendable { + /// A wrapper to silence `Sendable` warnings for `JSONEncoder` when not on macOS. + struct FakeSendable: @unchecked Sendable { let value: T } + + /// The `JSONEncoder` used for encoding values that can't be directly converted. + let json: FakeSendable + + public init(json: JSONEncoder = .init()) { + self.json = .init(value: json) + } + public func encode(_ value: any Encodable) throws -> SQLiteData { if let data = (value as? any SQLiteDataConvertible)?.sqliteData { return data } else { - let encoder = EncoderImpl() + let encoder = NestedSingleValueUnwrappingEncoder(dataEncoder: self) - try value.encode(to: encoder) - switch encoder.result { - case .data(let data): - return data - case .unkeyed, .keyed: + do { + try value.encode(to: encoder) + guard let value = encoder.value else { + throw SQLCodingError.unsupportedOperation("missing value", codingPath: []) + } + return value + } catch is SQLCodingError { // 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 @@ -24,60 +35,127 @@ 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 .text(.init(decoding: try JSONEncoder().encode(value), as: UTF8.self)) + // + // Update: SQLite 3.45.1 (2024-01-30) fixed the JSON-blob behavior, but as noted above, we prefer + // sending JSON as text anyway, so we've left it as-is. + return .text(.init(decoding: try self.json.value.encode(value), as: UTF8.self)) } } } - private enum Result { - case keyed - case unkeyed - case data(SQLiteData) - } - - 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] { [] } + /// A trivial encoder for unwrapping types which encode as trivial single-value containers. This allows for + /// correct handling of types such as `Optional` when they do not conform to `SQLiteDataConvertible`. + private final class NestedSingleValueUnwrappingEncoder: Encoder, SingleValueEncodingContainer { + // See `Encoder.userInfo`. var userInfo: [CodingUserInfoKey: Any] { [:] } - var result: Result - init() { self.result = .data(.null) } - + // See `Encoder.codingPath` and `SingleValueEncodingContainer.codingPath`. + var codingPath: [any CodingKey] { [] } + + /// The parent ``SQLiteDataEncoder``. + let dataEncoder: SQLiteDataEncoder + + /// Storage for the resulting converted value. + var value: SQLiteData? = nil + + /// Create a new encoder with an ``SQLiteDataEncoder``. + init(dataEncoder: SQLiteDataEncoder) { + self.dataEncoder = dataEncoder + } + + // See `Encoder.container(keyedBy:)`. func container(keyedBy: K.Type) -> KeyedEncodingContainer { - self.result = .keyed - return .init(KeyedEncoderImpl()) + .invalid(at: self.codingPath) } - + + // See `Encoder.unkeyedContainer`. func unkeyedContainer() -> any UnkeyedEncodingContainer { - self.result = .unkeyed - return UnkeyedEncoderImpl() + .invalid(at: self.codingPath) + } + + // See `Encoder.singleValueContainer`. + func singleValueContainer() -> any SingleValueEncodingContainer { + self + } + + // See `SingleValueEncodingContainer.encodeNil()`. + func encodeNil() throws { + self.value = .null + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Bool) throws { + self.value = .integer(value ? 1 : 0) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: String) throws { + self.value = .text(value) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Float) throws { + self.value = .float(Double(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Double) throws { + self.value = .float(value) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Int8) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Int16) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Int32) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Int64) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: Int) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: UInt8) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: UInt16) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: UInt32) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: UInt64) throws { + self.value = .integer(numericCast(value)) + } + + // See `SingleValueEncodingContainer.encode(_:)`. + func encode(_ value: UInt) throws { + self.value = .integer(numericCast(value)) } - - func singleValueContainer() -> any SingleValueEncodingContainer { self } - - func encodeNil() throws { self.result = .data(.null) } + // See `SingleValueEncodingContainer.encode(_:)`. func encode(_ value: some Encodable) throws { - self.result = .data(try SQLiteDataEncoder().encode(value)) + self.value = try self.dataEncoder.encode(value) } } } diff --git a/Sources/SQLiteKit/SQLiteDialect.swift b/Sources/SQLiteKit/SQLiteDialect.swift index c0031fb..ff54511 100644 --- a/Sources/SQLiteKit/SQLiteDialect.swift +++ b/Sources/SQLiteKit/SQLiteDialect.swift @@ -46,6 +46,6 @@ public struct SQLiteDialect: SQLDialect { public init() {} private func isAtLeastVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool { - _SQLiteDatabaseVersion.runtimeVersion.isNotOlder(than: _SQLiteDatabaseVersion(major: major, minor: minor, patch: patch)) + SQLiteDatabaseVersion.runtimeVersion >= SQLiteDatabaseVersion(major: major, minor: minor, patch: patch) } } diff --git a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift index 75081cf..ea944ad 100644 --- a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift +++ b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift @@ -1,30 +1,53 @@ import SQLKit import SQLiteNIO -extension SQLiteRow: SQLRow { - public var allColumns: [String] { - self.columns.map { $0.name } +extension SQLiteRow { + /// Return an `SQLRow` interface to this row. + /// + /// - Parameter decoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in + /// `SQLRow`s. + /// - Returns: An instance of `SQLRow` which accesses the same data as `self`. + func sql(decoder: SQLiteDataDecoder = .init()) -> any SQLRow { + SQLiteSQLRow(row: self, decoder: decoder) + } +} + +/// An error used to signal that a column requested from a `MySQLRow` using the `SQLRow` interface is not present. +struct MissingColumn: Error { + let column: String +} + +/// Wraps an `SQLiteRow` with the `SQLRow` protocol. +private struct SQLiteSQLRow: SQLRow { + /// The underlying `SQLiteRow`. + let row: SQLiteRow + + /// A ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values. + let decoder: SQLiteDataDecoder + + // See `SQLRow.allColumns`. + var allColumns: [String] { + self.row.columns.map { $0.name } } - public func decodeNil(column: String) throws -> Bool { - guard let data = self.column(column) else { + // See `SQLRow.contains(column:)`. + func contains(column: String) -> Bool { + self.row.column(column) != nil + } + + // See `SQLRow.decodeNil(column:)`. + func decodeNil(column: String) throws -> Bool { + guard let data = self.row.column(column) else { return true } return data == .null } - public func contains(column: String) -> Bool { - self.column(column) != nil - } - - public func decode(column: String, as: D.Type) throws -> D { - guard let data = self.column(column) else { + // See `SQLRow.decode(column:as:)`. + func decode(column: String, as: D.Type) throws -> D { + guard let data = self.row.column(column) else { throw MissingColumn(column: column) } - return try SQLiteDataDecoder().decode(D.self, from: data) + return try self.decoder.decode(D.self, from: data) } } - -struct MissingColumn: Error { - let column: String -} From 433d85163e55e6e1467d65a60506810c31d0756a Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 1 May 2024 07:16:31 -0500 Subject: [PATCH 03/11] Add 100% doc comments coverage --- Sources/SQLiteKit/SQLiteConfiguration.swift | 44 ++++++-- .../SQLiteKit/SQLiteConnection+SQLKit.swift | 6 + .../SQLiteKit/SQLiteConnectionSource.swift | 22 ++++ Sources/SQLiteKit/SQLiteDataDecoder.swift | 10 ++ Sources/SQLiteKit/SQLiteDataEncoder.swift | 8 ++ Sources/SQLiteKit/SQLiteDialect.swift | 106 ++++++++++++++---- 6 files changed, 164 insertions(+), 32 deletions(-) diff --git a/Sources/SQLiteKit/SQLiteConfiguration.swift b/Sources/SQLiteKit/SQLiteConfiguration.swift index 9177522..66e8857 100644 --- a/Sources/SQLiteKit/SQLiteConfiguration.swift +++ b/Sources/SQLiteKit/SQLiteConfiguration.swift @@ -1,32 +1,52 @@ import struct Foundation.UUID +/// Describes a configuration for an SQLite database connection. public struct SQLiteConfiguration: Sendable { + /// The possible storage types for an SQLite database. public enum Storage: Sendable { - /// Stores the SQLite database in memory. - /// - /// Uses a randomly generated identifier. See `memory(identifier:)`. + /// Specify an SQLite database stored in memory, using a randomly generated identifier. + /// + /// See ``memory(identifier:)``. public static var memory: Self { .memory(identifier: UUID().uuidString) } - /// Stores the SQLite database in memory. - /// - parameters: - /// - identifier: Uniquely identifies the in-memory storage. - /// Connections using the same identifier share data. + /// Specify an SQLite database stored in memory, using a given identifier string. + /// + /// An in-memory database persists only until the last connection to it is closed. If a new connection is + /// opened after that point, even using the same identifier, a new, empty database is created. + /// + /// - Parameter identifier: Uniquely identifies the in-memory storage. Multiple connections may use this + /// identifier to connect to the same in-memory storage for the duration of its lifetime. The identifer + /// has no predefined format or restrictions on its content. case memory(identifier: String) - /// Uses the SQLite database file at the specified path. + /// Specify an SQLite database stored in a file at the specified path. /// - /// Non-absolute paths will check the current working directory. + /// If a relative path is specified, it is interpreted relative to the current working directory of the + /// current process (e.g. `NIOFileSystem.shared.currentWorkingDirectory`). It is recommended to always use + /// absolute paths whenever possible. + /// + /// - Parameter path: The filesystem path at which to store the database. case file(path: String) } - + + /// The storage type for the database. + /// + /// See ``Storage-swift.enum`` for the available storage types. public var storage: Storage - /// If `true`, foreign keys will be enabled automatically on new connections. + /// When `true`, foreign key support is automatically enabled on all connections using this configuration. + /// + /// Internally issues a `PRAGMA foreign_keys = ON` query when enabled. public var enableForeignKeys: Bool - /// Creates a new `SQLiteConfiguration`. + /// Create a new ``SQLiteConfiguration``. + /// + /// - Parameters: + /// - storage: The storage type to use for the database. See ``Storage-swift.enum``. + /// - enableForeignKeys: Whether to enable foreign key support by default for all connections. + /// Defaults to `true`. public init(storage: Storage, enableForeignKeys: Bool = true) { self.storage = storage self.enableForeignKeys = enableForeignKeys diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift index c570648..d14f8ac 100644 --- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift +++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift @@ -124,18 +124,23 @@ private struct SQLiteSQLDatabase: SQLDatabase { /// An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. let decoder: SQLiteDataDecoder + + // See `SQLDatabase.eventLoop`. var eventLoop: any EventLoop { self.database.value.eventLoop } + // See `SQLDatabase.version`. var version: (any SQLDatabaseReportedVersion)? { SQLiteDatabaseVersion.runtimeVersion } + // See `SQLDatabase.logger`. var logger: Logger { self.database.value.logger } + // See `SQLDatabase.dialect`. var dialect: any SQLDialect { SQLiteDialect() } @@ -143,6 +148,7 @@ private struct SQLiteSQLDatabase: SQLDatabase { // See `SQLDatabase.queryLogLevel`. var queryLogLevel: Logger.Level? + // See `SQLDatabase.execute(sql:_:)`. func execute( sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift index ac40710..506fc89 100644 --- a/Sources/SQLiteKit/SQLiteConnectionSource.swift +++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift @@ -5,6 +5,7 @@ import NIOPosix import SQLiteNIO import NIOCore +/// A `ConnectionPoolSource` providing SQLite database connections for a given ``SQLiteConfiguration``. public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { private let configuration: SQLiteConfiguration private let actualURL: URL @@ -14,6 +15,15 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { .file(path: self.actualURL.absoluteString) } + /// Create a new ``SQLiteConnectionSource``. + /// + /// > Important: If the caller provides a thread pool other than the default, they are responsible for starting + /// > the pool before any connections are made and shutting it down only after all connections are closed. It is + /// > strongly recommended that all callers use the default. + /// + /// - Parameters: + /// - configuration: The configuration for new connections. + /// - threadPool: The thread pool used by connections. Defaults to the global singleton. public init( configuration: SQLiteConfiguration, threadPool: NIOThreadPool = .singleton @@ -23,6 +33,7 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { self.threadPool = threadPool } + // See `ConnectionPoolSource.makeConnection(logger:on:)`. public func makeConnection( logger: Logger, on eventLoop: any EventLoop @@ -46,6 +57,7 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable { extension SQLiteNIO.SQLiteConnection: AsyncKit.ConnectionPoolItem {} fileprivate extension String { + /// Attempt to "sanitize" a string for use as a filename. This is quick and dirty and probably not 100% correct. var asSafeFilename: String { #if os(Windows) self.replacingOccurrences(of: ":", with: "_").replacingOccurrences(of: "\\", with: "-") @@ -56,6 +68,16 @@ fileprivate extension String { } fileprivate extension SQLiteConfiguration.Storage { + /// Because SQLiteNIO specifies the recommended `SQLITE_OMIT_SHARED_CACHE` build flag, we cannot implement our + /// claimed support for multiple connections to in-memory databases using actual in-memory databases. + /// Unfortunately, Fluent relies on having this support, and it had been public API since long before the change + /// in build flags. Therefore, we work around it by using temporary files to fake in-memory databases. + /// + /// This has the unfortunate side effect of violating the "when the last connection to an in-memory database is + /// closed, it is immediately deleted" semantics, but fortunately no one seems to have relied on that behavior. + /// + /// We include both the user-provided identifer and the current process ID in the filename for the temporary + /// file because in-memory databases are expected to be process-specific. var urlForSQLite: URL { switch self { case .memory(identifier: let identifier): diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift index 115b8d4..973760b 100644 --- a/Sources/SQLiteKit/SQLiteDataDecoder.swift +++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift @@ -3,6 +3,10 @@ import SQLiteNIO @_spi(CodableUtilities) import SQLKit import NIOFoundationCompat +/// Translates `SQLiteData` values received from the database into `Decodable` values. +/// +/// Types which conform to `SQLiteDataConvertible` are converted directly to the requested type. For other types, +/// an attempt is made to interpret the database value as JSON and decode the type from it. public struct SQLiteDataDecoder: Sendable { /// A wrapper to silence `Sendable` warnings for `JSONDecoder` when not on macOS. struct FakeSendable: @unchecked Sendable { let value: T } @@ -18,6 +22,12 @@ public struct SQLiteDataDecoder: Sendable { self.json = .init(value: json) } + /// Convert the given `SQLiteData` into a value of type `T`, if possible. + /// + /// - Parameters: + /// - type: The desired result type. + /// - data: The data to decode. + /// - Returns: The decoded value, if successful. 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 { diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift index 613e0a0..27da7d1 100644 --- a/Sources/SQLiteKit/SQLiteDataEncoder.swift +++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift @@ -3,6 +3,10 @@ import Foundation @_spi(CodableUtilities) import SQLKit import SQLiteNIO +/// Translates `Encodable` values into `SQLiteData` values suitable for use with an `SQLiteDatabase`. +/// +/// Types which conform to `SQLiteDataConvertible` are converted directly to `SQLiteData`. Other types are +/// encoded as JSON and sent to the database as text. public struct SQLiteDataEncoder: Sendable { /// A wrapper to silence `Sendable` warnings for `JSONEncoder` when not on macOS. struct FakeSendable: @unchecked Sendable { let value: T } @@ -14,6 +18,10 @@ public struct SQLiteDataEncoder: Sendable { self.json = .init(value: json) } + /// Convert the given `Encodable` value to an `SQLiteData` value, if possible. + /// + /// - Parameter value: The value to convert. + /// - Returns: A converted `SQLiteData` value, if successful. public func encode(_ value: any Encodable) throws -> SQLiteData { if let data = (value as? any SQLiteDataConvertible)?.sqliteData { return data diff --git a/Sources/SQLiteKit/SQLiteDialect.swift b/Sources/SQLiteKit/SQLiteDialect.swift index ff54511..1f4c55b 100644 --- a/Sources/SQLiteKit/SQLiteDialect.swift +++ b/Sources/SQLiteKit/SQLiteDialect.swift @@ -2,29 +2,95 @@ import SQLKit /// The ``SQLDialect`` defintions for SQLite. /// -/// - Note: There is only ever one SQLite library in use by SQLiteNIO in any given process (even if there are -/// other versions of the library being used by other things). As such, there is no need for the dialect to -/// concern itself with what version the connection using it "connected" to - it can always just look up the -/// global "runtime version". +/// > Note: There is only ever one SQLite library in use by SQLiteNIO in any given process (even if there are +/// > other versions of the library being used by other things). As such, there is no need for the dialect to +/// > concern itself with what version the connection using it "connected" to - it can always just look up the +/// > global "runtime version". public struct SQLiteDialect: SQLDialect { - public var name: String { "sqlite" } + /// Create a new ``SQLiteDialect``. + public init() {} + + // See `SQLDialect.name`. + public var name: String { + "sqlite" + } + + // See `SQLDialect.identifierQuote`. + public var identifierQuote: any SQLExpression { + SQLRaw(#"""#) + } + + // See `SQLDialect.literalStringQuote`. + public var literalStringQuote: any SQLExpression { + SQLRaw("'") + } + + // See `SQLDialect.bindPlaceholder(at:)`. + public func bindPlaceholder(at position: Int) -> any SQLExpression { + SQLRaw("?\(position)") + } + + // See `SQLDialect.literalBoolean(_:)`. + public func literalBoolean(_ value: Bool) -> any SQLExpression { + SQLRaw(value ? "TRUE" : "FALSE") + } + + // See `SQLDialect.literalDefault`. + public var literalDefault: any SQLExpression { + SQLLiteral.null + } + + // See `SQLDialect.supportsIfExists`. + public var supportsIfExists: Bool { + true + } - public var identifierQuote: any SQLExpression { SQLRaw("\"") } - public var literalStringQuote: any SQLExpression { SQLRaw("'") } - public func bindPlaceholder(at position: Int) -> any SQLExpression { SQLRaw("?\(position)") } - public func literalBoolean(_ value: Bool) -> any SQLExpression { SQLRaw(value ? "TRUE" : "FALSE") } - public var literalDefault: any SQLExpression { SQLLiteral.null } + // See `SQLDialect.supportsDropBehavior`. + public var supportsDropBehavior: Bool { + false + } - public var supportsAutoIncrement: Bool { false } - public var autoIncrementClause: any SQLExpression { SQLRaw("AUTOINCREMENT") } + // See `SQLDialect.supportsAutoIncrement`. + public var supportsAutoIncrement: Bool { + false + } + + // See `SQLDialect.autoIncrementClause`. + public var autoIncrementClause: any SQLExpression { + SQLRaw("AUTOINCREMENT") + } - public var enumSyntax: SQLEnumSyntax { .unsupported } - public var triggerSyntax: SQLTriggerSyntax { .init(create: [.supportsBody, .supportsCondition]) } - public var alterTableSyntax: SQLAlterTableSyntax { .init(allowsBatch: false) } - public var upsertSyntax: SQLUpsertSyntax { self.isAtLeastVersion(3, 24, 0) ? .standard : .unsupported } // `UPSERT` was added to SQLite in 3.24.0. - public var supportsReturning: Bool { self.isAtLeastVersion(3, 35, 0) } // `RETURNING` was added to SQLite in 3.35.0. - public var unionFeatures: SQLUnionFeatures { [.union, .unionAll, .intersect, .except] } + // See `SQLDialect.enumSyntax`. + public var enumSyntax: SQLEnumSyntax { + .unsupported + } + + // See `SQLDialect.triggerSyntax`. + public var triggerSyntax: SQLTriggerSyntax { + .init(create: [.supportsBody, .supportsCondition, .supportsUpdateColumns], drop: []) + } + // See `SQLDialect.alterTableSyntax`. + public var alterTableSyntax: SQLAlterTableSyntax { + .init(allowsBatch: false) + } + + // See `SQLDialect.upsertSyntax`. + public var upsertSyntax: SQLUpsertSyntax { + self.isAtLeastVersion(3, 24, 0) ? .standard : .unsupported // `UPSERT` was added to SQLite in 3.24.0. + } + + // See `SQLDialect.supportsReturning`. + public var supportsReturning: Bool { + self.isAtLeastVersion(3, 35, 0) // `RETURNING` was added to SQLite in 3.35.0. + } + + // See `SQLDialect.unionFeatures`. + public var unionFeatures: SQLUnionFeatures { + [.union, .unionAll, .intersect, .except] + } + + // See `SQLDialect.customDataType(for:)`. public func customDataType(for dataType: SQLDataType) -> (any SQLExpression)? { if case .bigint = dataType { // Translate requests for bigint to requests for SQLite's plain integer type. This yields the autoincrement @@ -34,6 +100,7 @@ public struct SQLiteDialect: SQLDialect { return nil } + // See `SQLDialect.nestedSubpathExpression(in:for:)`. public func nestedSubpathExpression(in column: any SQLExpression, for path: [String]) -> (any SQLExpression)? { guard !path.isEmpty else { return nil } @@ -43,8 +110,7 @@ public struct SQLiteDialect: SQLDialect { ]) } - public init() {} - + /// Convenience utility for checking current SQLite version. private func isAtLeastVersion(_ major: Int, _ minor: Int, _ patch: Int) -> Bool { SQLiteDatabaseVersion.runtimeVersion >= SQLiteDatabaseVersion(major: major, minor: minor, patch: patch) } From ee587c27f8e25fdfae9e659abf86798a813df99b Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Wed, 1 May 2024 07:16:41 -0500 Subject: [PATCH 04/11] Use un-deprecated SQLBenchmarker API --- Tests/SQLiteKitTests/SQLiteKitTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SQLiteKitTests/SQLiteKitTests.swift b/Tests/SQLiteKitTests/SQLiteKitTests.swift index c9cd241..599a9f0 100644 --- a/Tests/SQLiteKitTests/SQLiteKitTests.swift +++ b/Tests/SQLiteKitTests/SQLiteKitTests.swift @@ -6,10 +6,10 @@ import SQLiteNIO import SQLKit final class SQLiteKitTests: XCTestCase { - func testSQLKitBenchmark() throws { + func testSQLKitBenchmark() async throws { let benchmark = SQLBenchmarker(on: self.db) - try benchmark.run() + try await benchmark.runAllTests() } func testPlanets() async throws { From c68a05cb8246eeacc2f09f70be0a063db214a929 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 2 May 2024 01:58:25 -0500 Subject: [PATCH 05/11] SQLiteRow.sql(decoder:) needs to be public for the benefit of FluentSQLiteDriver. --- Sources/SQLiteKit/SQLiteRow+SQLRow.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift index ea944ad..9c8d79d 100644 --- a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift +++ b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift @@ -7,7 +7,7 @@ extension SQLiteRow { /// - Parameter decoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in /// `SQLRow`s. /// - Returns: An instance of `SQLRow` which accesses the same data as `self`. - func sql(decoder: SQLiteDataDecoder = .init()) -> any SQLRow { + public func sql(decoder: SQLiteDataDecoder = .init()) -> any SQLRow { SQLiteSQLRow(row: self, decoder: decoder) } } From 6773aadf01a6d83603f8303dc7cdf0e3adeccd4c Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sat, 4 May 2024 12:02:50 -0500 Subject: [PATCH 06/11] Remove not yet available upcoming/experimental feature flags from the 5.8 manifest --- Package.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Package.swift b/Package.swift index 2ec2375..9bd8a18 100644 --- a/Package.swift +++ b/Package.swift @@ -43,6 +43,4 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ConciseMagicFile"), .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableExperimentalFeature("StrictConcurrency=complete"), ] } From 173802261c34c5b0ebeb425f1eb582181f8c4409 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 9 May 2024 22:55:51 -0500 Subject: [PATCH 07/11] Update dep minimums --- Package.swift | 2 +- Package@swift-5.9.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 9bd8a18..c9ef1eb 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,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/apple/swift-nio.git", from: "2.65.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.29.3"), .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index f5f9fe7..67965f2 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -13,7 +13,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/apple/swift-nio.git", from: "2.65.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.29.3"), .package(url: "https://github.com/vapor/async-kit.git", from: "1.19.0"), From 2274825c1b45bf6d7290204a5132027f847c3ec3 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Thu, 9 May 2024 22:56:31 -0500 Subject: [PATCH 08/11] Take advantage of further improvements in SQLiteNIO - SQLiteDatabase is Sendable, support queryLogLevel, don't do extra thread hops in withSession() --- .../SQLiteKit/SQLiteConnection+SQLKit.swift | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift index d14f8ac..c2447ef 100644 --- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift +++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift @@ -1,5 +1,6 @@ import SQLKit import SQLiteNIO +import Logging extension SQLiteDatabase { /// Return an object allowing access to this database via the `SQLDatabase` interface. @@ -10,9 +11,10 @@ extension SQLiteDatabase { /// - Returns: An instance of `SQLDatabase` which accesses the same database as `self`. public func sql( encoder: SQLiteDataEncoder = .init(), - decoder: SQLiteDataDecoder = .init() + decoder: SQLiteDataDecoder = .init(), + queryLogLevel: Logger.Level? = .debug ) -> any SQLDatabase { - SQLiteSQLDatabase(database: .init(value: self), encoder: encoder, decoder: decoder) + SQLiteSQLDatabase(database: self, encoder: encoder, decoder: decoder, queryLogLevel: queryLogLevel) } } @@ -111,13 +113,8 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { /// Wraps a `SQLiteDatabase` with the `SQLDatabase` protocol. private struct SQLiteSQLDatabase: SQLDatabase { - /// A trivial wrapper type to work around Sendable warnings due to SQLiteNIO not being Sendable-correct. - struct FakeSendable: @unchecked Sendable { - let value: T - } - /// The underlying database. - let database: FakeSendable + let database: D /// An ``SQLiteDataEncoder`` used to translate bindings into `SQLiteData` values. let encoder: SQLiteDataEncoder @@ -127,7 +124,7 @@ private struct SQLiteSQLDatabase: SQLDatabase { // See `SQLDatabase.eventLoop`. var eventLoop: any EventLoop { - self.database.value.eventLoop + self.database.eventLoop } // See `SQLDatabase.version`. @@ -137,7 +134,7 @@ private struct SQLiteSQLDatabase: SQLDatabase { // See `SQLDatabase.logger`. var logger: Logger { - self.database.value.logger + self.database.logger } // See `SQLDatabase.dialect`. @@ -146,28 +143,29 @@ private struct SQLiteSQLDatabase: SQLDatabase { } // See `SQLDatabase.queryLogLevel`. - var queryLogLevel: Logger.Level? + let queryLogLevel: Logger.Level? // See `SQLDatabase.execute(sql:_:)`. func execute( sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () ) -> EventLoopFuture { - var serializer = SQLSerializer(database: self) - query.serialize(to: &serializer) + let (sql, rawBinds) = self.serialize(query) + + if let queryLogLevel = self.queryLogLevel { + self.logger.log(level: queryLogLevel, "\(sql) [\(rawBinds)]") + } + let binds: [SQLiteData] do { - binds = try serializer.binds.map { encodable in - try self.encoder.encode(encodable) - } + binds = try rawBinds.map { try self.encoder.encode($0) } } catch { return self.eventLoop.makeFailedFuture(error) } - return self.database.value.query( - serializer.sql, + return self.database.query( + sql, binds, - logger: self.logger, { onRow($0.sql(decoder: self.decoder)) } ) } @@ -177,25 +175,23 @@ private struct SQLiteSQLDatabase: SQLDatabase { sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () ) async throws { - var serializer = SQLSerializer(database: self) - query.serialize(to: &serializer) - - let binds = try serializer.binds.map { try self.encoder.encode($0) } + let (sql, rawBinds) = self.serialize(query) - return try await self.database.value.query( - serializer.sql, - binds, - logger: self.logger, + if let queryLogLevel = self.queryLogLevel { + self.logger.log(level: queryLogLevel, "\(sql) [\(rawBinds)]") + } + + try await self.database.query( + sql, + rawBinds.map { try self.encoder.encode($0) }, { onRow($0.sql(decoder: self.decoder)) } - ).get() + ) } // See `SQLDatabase.withSession(_:)`. func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { - try await self.database.value.withConnection { c in - c.eventLoop.makeFutureWithTask { - try await closure(c.sql(encoder: self.encoder, decoder: self.decoder)) - } - }.get() + try await self.database.withConnection { + try await closure($0.sql(encoder: self.encoder, decoder: self.decoder, queryLogLevel: self.queryLogLevel)) + } } } From 631b856af805c8ee52e6546771cb255d2db7057c Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Fri, 10 May 2024 19:43:04 -0500 Subject: [PATCH 09/11] Add back (deprecated) direct conformance to SQLRow to SQLiteRow (removing the conformance was source-breaking) --- Sources/SQLiteKit/SQLiteRow+SQLRow.swift | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift index 9c8d79d..77edf2f 100644 --- a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift +++ b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift @@ -51,3 +51,40 @@ private struct SQLiteSQLRow: SQLRow { return try self.decoder.decode(D.self, from: data) } } + +/// A legacy deprecated conformance of `SQLiteRow` directly to `SQLRow`. This interface exists solely +/// because its absence would be a public API break. +/// +/// Do not use these methods directly. Call `sql(decoder:)` instead to access `SQLiteRow`s through +/// an `SQLKit` interface. +@available(*, deprecated, message: "Use SQLiteRow.sql(decoder:) to access an SQLiteRow as an SQLRow.") +extension SQLiteNIO.SQLiteRow: SQLKit.SQLRow { + // See `SQLRow.allColumns`. + public var allColumns: [String] { self.columns.map { $0.name } } + + // See `SQLRow.contains(column:)`. + public func contains(column: String) -> Bool { self.column(column) != nil } + + // See `SQLRow.decodeNil(column:)`. + public func decodeNil(column: String) throws -> Bool { (self.column(column) ?? .null) == .null } + + // See `SQLRow.decode(column:as:)`. + public func decode(column: String, as: D.Type) throws -> D { + guard let data = self.column(column) else { throw MissingColumn(column: column) } + return try SQLiteDataDecoder().decode(D.self, from: data) + } + + // See `SQLRow.decode(column:inferringAs:)`. + public func decode(column c: String, inferringAs: D.Type = D.self) throws -> D { try self.decode(column: c, as: D.self) } + + // See `SQLRow.decode(model:prefix:keyDecodingStrategy:userInfo:)`. + public func decode( + model: D.Type, prefix: String? = nil, keyDecodingStrategy: SQLRowDecoder.KeyDecodingStrategy = .useDefaultKeys, + userInfo: [CodingUserInfoKey: any Sendable] = [:] + ) throws -> D { + try self.decode(model: D.self, with: .init(prefix: prefix, keyDecodingStrategy: keyDecodingStrategy, userInfo: userInfo)) + } + + // See `SQLRow.decode(model:with:)`. + public func decode(model: D.Type, with: SQLRowDecoder) throws -> D { try with.decode(D.self, from: self) } +} From 17b88c32acc6ad8f2917214180996c7ff5451aa4 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 12 May 2024 00:25:34 -0500 Subject: [PATCH 10/11] Pedantically avoid API breakage with SQLiteDatabase.sql() and make it @inlinable, fix a Sendability warning, add a missing doc comment. --- .../SQLiteKit/SQLiteConnection+SQLKit.swift | 75 ++++++++++++++++++- .../SQLiteKit/SQLiteConnectionSource.swift | 4 + Sources/SQLiteKit/SQLiteDataEncoder.swift | 4 + 3 files changed, 79 insertions(+), 4 deletions(-) diff --git a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift index c2447ef..02950bf 100644 --- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift +++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift @@ -2,17 +2,64 @@ import SQLKit import SQLiteNIO import Logging +// Hint: Yes, I know what default arguments are. This ridiculous spelling out of each alternative avoids public API +// breakage from adding the defaults. + extension SQLiteDatabase { + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql() -> any SQLDatabase { + self.sql(encoder: .init(), decoder: .init(), queryLogLevel: .debug) + } + + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql(encoder: SQLiteDataEncoder) -> any SQLDatabase { + self.sql(encoder: encoder, decoder: .init(), queryLogLevel: .debug) + } + + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql(decoder: SQLiteDataDecoder) -> any SQLDatabase { + self.sql(encoder: .init(), decoder: decoder, queryLogLevel: .debug) + } + + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql(encoder: SQLiteDataEncoder, decoder: SQLiteDataDecoder) -> any SQLDatabase { + self.sql(encoder: encoder, decoder: decoder, queryLogLevel: .debug) + } + + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql(queryLogLevel: Logger.Level?) -> any SQLDatabase { + self.sql(encoder: .init(), decoder: .init(), queryLogLevel: queryLogLevel) + } + + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql(encoder: SQLiteDataEncoder, queryLogLevel: Logger.Level?) -> any SQLDatabase { + self.sql(encoder: encoder, decoder: .init(), queryLogLevel: queryLogLevel) + } + + /// Shorthand for ``sql(encoder:decoder:queryLogLevel:)``. + @inlinable + public func sql(decoder: SQLiteDataDecoder, queryLogLevel: Logger.Level?) -> any SQLDatabase { + self.sql(encoder: .init(), decoder: decoder, queryLogLevel: queryLogLevel) + } + /// Return an object allowing access to this database via the `SQLDatabase` interface. /// /// - Parameters: /// - encoder: An ``SQLiteDataEncoder`` used to translate bound query parameters into `SQLiteData` values. /// - decoder: An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. + /// - queryLogLevel: The level at which SQL queries issued through the SQLKit interface will be logged. /// - Returns: An instance of `SQLDatabase` which accesses the same database as `self`. + @inlinable public func sql( - encoder: SQLiteDataEncoder = .init(), - decoder: SQLiteDataDecoder = .init(), - queryLogLevel: Logger.Level? = .debug + encoder: SQLiteDataEncoder, + decoder: SQLiteDataDecoder, + queryLogLevel: Logger.Level? ) -> any SQLDatabase { SQLiteSQLDatabase(database: self, encoder: encoder, decoder: decoder, queryLogLevel: queryLogLevel) } @@ -112,40 +159,58 @@ struct SQLiteDatabaseVersion: SQLDatabaseReportedVersion { } /// Wraps a `SQLiteDatabase` with the `SQLDatabase` protocol. -private struct SQLiteSQLDatabase: SQLDatabase { +@usableFromInline +/*private*/ struct SQLiteSQLDatabase: SQLDatabase { /// The underlying database. + @usableFromInline let database: D /// An ``SQLiteDataEncoder`` used to translate bindings into `SQLiteData` values. + @usableFromInline let encoder: SQLiteDataEncoder /// An ``SQLiteDataDecoder`` used to translate `SQLiteData` values into output values in `SQLRow`s. + @usableFromInline let decoder: SQLiteDataDecoder // See `SQLDatabase.eventLoop`. + @usableFromInline var eventLoop: any EventLoop { self.database.eventLoop } // See `SQLDatabase.version`. + @usableFromInline var version: (any SQLDatabaseReportedVersion)? { SQLiteDatabaseVersion.runtimeVersion } // See `SQLDatabase.logger`. + @usableFromInline var logger: Logger { self.database.logger } // See `SQLDatabase.dialect`. + @usableFromInline var dialect: any SQLDialect { SQLiteDialect() } // See `SQLDatabase.queryLogLevel`. + @usableFromInline let queryLogLevel: Logger.Level? + @inlinable + init(database: D, encoder: SQLiteDataEncoder, decoder: SQLiteDataDecoder, queryLogLevel: Logger.Level?) { + self.database = database + self.encoder = encoder + self.decoder = decoder + self.queryLogLevel = queryLogLevel + } + // See `SQLDatabase.execute(sql:_:)`. + @usableFromInline func execute( sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () @@ -171,6 +236,7 @@ private struct SQLiteSQLDatabase: SQLDatabase { } // See `SQLDatabase.execute(sql:_:)`. + @usableFromInline func execute( sql query: any SQLExpression, _ onRow: @escaping @Sendable (any SQLRow) -> () @@ -189,6 +255,7 @@ private struct SQLiteSQLDatabase: SQLDatabase { } // See `SQLDatabase.withSession(_:)`. + @usableFromInline func withSession(_ closure: @escaping @Sendable (any SQLDatabase) async throws -> R) async throws -> R { try await self.database.withConnection { try await closure($0.sql(encoder: self.encoder, decoder: self.decoder, queryLogLevel: self.queryLogLevel)) diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift index 506fc89..f946f36 100644 --- a/Sources/SQLiteKit/SQLiteConnectionSource.swift +++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift @@ -1,4 +1,8 @@ +#if canImport(Darwin) import Foundation +#else +@preconcurrency import Foundation +#endif import Logging import AsyncKit import NIOPosix diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift index 27da7d1..f2c0335 100644 --- a/Sources/SQLiteKit/SQLiteDataEncoder.swift +++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift @@ -14,6 +14,10 @@ public struct SQLiteDataEncoder: Sendable { /// The `JSONEncoder` used for encoding values that can't be directly converted. let json: FakeSendable + /// Initialize a ``SQLiteDataEncoder`` with a JSON encoder. + /// + /// - Parameter json: A `JSONEncoder` to use for encoding types that can't be directly converted. Defaults + /// to an unconfigured encoder. public init(json: JSONEncoder = .init()) { self.json = .init(value: json) } From e9eae2999cc7e3fe17adaf3b6d6e62a8e156db1a Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sun, 12 May 2024 00:30:35 -0500 Subject: [PATCH 11/11] API breakage pedantry. --- Sources/SQLiteKit/SQLiteDataDecoder.swift | 10 +++++++--- Sources/SQLiteKit/SQLiteDataEncoder.swift | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Sources/SQLiteKit/SQLiteDataDecoder.swift b/Sources/SQLiteKit/SQLiteDataDecoder.swift index 973760b..68fe89a 100644 --- a/Sources/SQLiteKit/SQLiteDataDecoder.swift +++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift @@ -14,11 +14,15 @@ public struct SQLiteDataDecoder: Sendable { /// The `JSONDecoder` used for decoding values that can't be directly converted. let json: FakeSendable + /// Initialize a ``SQLiteDataDecoder`` with an unconfigured JSON decoder. + public init() { + self.init(json: .init()) + } + /// Initialize a ``SQLiteDataDecoder`` with a JSON decoder. /// - /// - Parameter json: A `JSONDecoder` to use for decoding types that can't be directly converted. Defaults - /// to an unconfigured decoder. - public init(json: JSONDecoder = .init()) { + /// - Parameter json: A `JSONDecoder` to use for decoding types that can't be directly converted. + public init(json: JSONDecoder) { self.json = .init(value: json) } diff --git a/Sources/SQLiteKit/SQLiteDataEncoder.swift b/Sources/SQLiteKit/SQLiteDataEncoder.swift index f2c0335..97e7ed3 100644 --- a/Sources/SQLiteKit/SQLiteDataEncoder.swift +++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift @@ -14,11 +14,15 @@ public struct SQLiteDataEncoder: Sendable { /// The `JSONEncoder` used for encoding values that can't be directly converted. let json: FakeSendable + /// Initialize a ``SQLiteDataEncoder`` with an unconfigured JSON encoder. + public init() { + self.init(json: .init()) + } + /// Initialize a ``SQLiteDataEncoder`` with a JSON encoder. /// - /// - Parameter json: A `JSONEncoder` to use for encoding types that can't be directly converted. Defaults - /// to an unconfigured encoder. - public init(json: JSONEncoder = .init()) { + /// - Parameter json: A `JSONEncoder` to use for encoding types that can't be directly converted. + public init(json: JSONEncoder) { self.json = .init(value: json) }