-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
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 bde2384..02950bf 100644
--- a/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
+++ b/Sources/SQLiteKit/SQLiteConnection+SQLKit.swift
@@ -1,39 +1,107 @@
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 {
- _SQLiteSQLDatabase(database: self)
+ 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,
+ decoder: SQLiteDataDecoder,
+ queryLogLevel: Logger.Level?
+ ) -> any SQLDatabase {
+ SQLiteSQLDatabase(database: self, encoder: encoder, decoder: decoder, queryLogLevel: queryLogLevel)
}
}
-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 +115,150 @@ 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.
+@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
+ 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 (any SQLRow) -> ()
+ _ 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 SQLiteDataEncoder().encode(encodable)
- }
+ binds = try rawBinds.map { try self.encoder.encode($0) }
} 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(
+ sql,
+ binds,
+ { onRow($0.sql(decoder: self.decoder)) }
+ )
+ }
+
+ // See `SQLDatabase.execute(sql:_:)`.
+ @usableFromInline
+ func execute(
+ sql query: any SQLExpression,
+ _ onRow: @escaping @Sendable (any SQLRow) -> ()
+ ) async throws {
+ let (sql, rawBinds) = self.serialize(query)
+
+ 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)) }
+ )
+ }
+
+ // 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))
}
- return self.database.query(serializer.sql, binds, logger: self.logger, onRowWorkaround)
}
}
-
diff --git a/Sources/SQLiteKit/SQLiteConnectionSource.swift b/Sources/SQLiteKit/SQLiteConnectionSource.swift
index 8768b31..f946f36 100644
--- a/Sources/SQLiteKit/SQLiteConnectionSource.swift
+++ b/Sources/SQLiteKit/SQLiteConnectionSource.swift
@@ -1,10 +1,15 @@
+#if canImport(Darwin)
import Foundation
+#else
+@preconcurrency import Foundation
+#endif
import Logging
import AsyncKit
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,20 +19,30 @@ 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
+ threadPool: NIOThreadPool = .singleton
) {
self.configuration = configuration
self.actualURL = configuration.storage.urlForSQLite
self.threadPool = threadPool
}
+ // See `ConnectionPoolSource.makeConnection(logger:on:)`.
public func makeConnection(
logger: Logger,
on eventLoop: any EventLoop
) -> EventLoopFuture {
- return SQLiteConnection.open(
+ SQLiteConnection.open(
storage: self.connectionStorage,
threadPool: self.threadPool,
logger: logger,
@@ -43,9 +58,10 @@ public struct SQLiteConnectionSource: ConnectionPoolSource, Sendable {
}
}
-extension SQLiteConnection: ConnectionPoolItem { }
+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 +72,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 71cecde..68fe89a 100644
--- a/Sources/SQLiteKit/SQLiteDataDecoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataDecoder.swift
@@ -1,28 +1,54 @@
import Foundation
import SQLiteNIO
+@_spi(CodableUtilities) import SQLKit
import NIOFoundationCompat
-public struct SQLiteDataDecoder {
- let json = JSONDecoder() // TODO: Add API to make this configurable
+/// 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 }
- public init() {}
+ /// 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.
+ public init(json: JSONDecoder) {
+ 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 {
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 +57,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] { [] }
- private struct GiftBoxUnwrapDecoder: Decoder, SingleValueDecodingContainer {
- let decoder: SQLiteDataDecoder
+ // See `Decoder.userInfo`.
+ var userInfo: [CodingUserInfoKey: Any] { [:] }
+
+ /// 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
+ }
- 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.decodeNil()`.
+ func decodeNil() -> Bool {
+ self.data.isNull
+ }
+
+ // 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..97e7ed3 100644
--- a/Sources/SQLiteKit/SQLiteDataEncoder.swift
+++ b/Sources/SQLiteKit/SQLiteDataEncoder.swift
@@ -1,21 +1,48 @@
import NIOCore
import Foundation
+@_spi(CodableUtilities) import SQLKit
import SQLiteNIO
-public struct SQLiteDataEncoder {
- public init() {}
-
+/// 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 }
+
+ /// 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.
+ public init(json: JSONEncoder) {
+ 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
} 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 +51,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..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,9 +110,8 @@ 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.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..77edf2f 100644
--- a/Sources/SQLiteKit/SQLiteRow+SQLRow.swift
+++ b/Sources/SQLiteKit/SQLiteRow+SQLRow.swift
@@ -1,30 +1,90 @@
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`.
+ public 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
- public func decodeNil(column: String) throws -> Bool {
- guard let data = self.column(column) else {
+ // See `SQLRow.allColumns`.
+ var allColumns: [String] {
+ self.row.columns.map { $0.name }
+ }
+
+ // 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
+ // 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 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)
- }
+ guard let data = self.column(column) else { throw MissingColumn(column: column) }
return try SQLiteDataDecoder().decode(D.self, from: data)
}
-}
-struct MissingColumn: Error {
- let column: String
+ // 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) }
}
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 {