This commit is contained in:
zeus
2025-01-28 12:28:03 +08:00
parent 738c373a77
commit ec96756800
2534 changed files with 486292 additions and 0 deletions
@@ -0,0 +1,488 @@
import Dispatch
import Foundation
public struct Configuration {
// MARK: - Misc options
/// A boolean value indicating whether foreign key support is enabled.
///
/// The default is true.
///
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>.
public var foreignKeysEnabled = true
/// A boolean value indicating whether an SQLite connection is read-only.
///
/// The default is false.
///
/// ```swift
/// var config = Configuration()
/// config.readonly = true
///
/// let dbQueue = try DatabaseQueue( // or DatabasePool
/// path: "/path/to/database.sqlite",
/// configuration: config)
/// ```
public var readonly = false
/// A label that describes a database connection.
///
/// You can query this label at runtime:
///
/// ```swift
/// var config = Configuration()
/// config.label = "MyDatabase"
/// let dbQueue = try DatabaseQueue(configuration: config)
///
/// try dbQueue.read { db in
/// print(db.configuration.label) // Prints "MyDatabase"
/// }
/// ```
///
/// The configuration label is also used to name ``Database`` connections
/// (their ``Database/description`` property), and the various dispatch
/// queues created by GRDB, visible in debugging sessions and crash logs.
///
/// Those connection names and dispatch queue labels are intended for
/// debugging only. Their format may change between GRDB releases.
/// Applications should not depend on connection names and dispatch
/// queue labels.
///
/// If the configuration label is nil, the current GRDB implementation uses
/// the following names:
///
/// - `GRDB.DatabaseQueue`: the (unique) connection of a DatabaseQueue
/// - `GRDB.DatabasePool.writer`: the (unique) writer connection of
/// a DatabasePool
/// - `GRDB.DatabasePool.reader.N`, where N is 1, 2, ...: one of the reader
/// connection(s) of a DatabasePool. N may get bigger than the maximum
/// number of concurrent readers, as SQLite connections get closed and new
/// ones are opened.
/// - `GRDB.DatabasePool.snapshot.N`: the connection of a DatabaseSnapshot.
/// N grows with the number of snapshots.
///
/// If the configuration label is not nil, for example "MyDatabase", the
/// current GRDB implementation uses the following names:
///
/// - `MyDatabase`: the (unique) connection of a DatabaseQueue
/// - `MyDatabase.writer`: the (unique) writer connection of a DatabasePool
/// - `MyDatabase.reader.N`, where N is 1, 2, ...: one of the reader
/// connection(s) of a DatabasePool. N may get bigger than the maximum
/// number of concurrent readers, as SQLite connections get closed and new
/// ones are opened.
/// - `MyDatabase.snapshot.N`: the connection of a DatabaseSnapshot. N grows
/// with the number of snapshots.
///
/// The default configuration label is nil.
public var label: String? = nil
/// A boolean value indicating whether SQLite 3.29+ interprets
/// double-quoted strings as string literals when they does not match any
/// valid identifier.
///
/// The default and recommended value is false:
///
/// ```swift
/// // Error: no such column: missingColumn
/// let name = try String.fetchOne(db, sql: """
/// SELECT "missingColumn" FROM "player"
/// """)
/// ```
///
/// When true, or before SQLite version 3.29.0, double-quoted strings that
/// do not match any valid identifier are interpreted as string literals,
/// as in the example below. This is an SQLite
/// [misfeature](https://sqlite.org/quirks.html#double_quoted_string_literals_are_accepted):
///
/// ```swift
/// // MISFEATURE: This query succeeds with result "missingColumn"
/// let name = try String.fetchOne(db, sql: """
/// SELECT "missingColumn" FROM "player"
/// """)
/// ```
public var acceptsDoubleQuotedStringLiterals = false
/// A boolean value indicating whether the database connection listens to
/// the ``Database/suspendNotification`` and ``Database/resumeNotification``
/// notifications.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// Set this flag to true when you apply the technique described in
/// <doc:DatabaseSharing#How-to-limit-the-0xDEAD10CC-exception>. See
/// ``Database/suspendNotification`` for more informations about
/// suspended databases.
public var observesSuspensionNotifications = false
/// A boolean value indicating whether statement arguments are visible in
/// the description of database errors and trace events.
///
/// The default and recommended value is false: statement arguments are not
/// visible in database errors and trace events, preventing sensitive
/// information from leaking in unexpected places. For example:
///
/// ```swift
/// db.trace { event in
/// // By default, sensitive information is NOT printed
/// // when a statement is traced:
/// print(event)
/// }
///
/// do {
/// // The sensitive information to protect
/// let email = "..."
/// let player = try Player.filter(Column("email") == email).fetchOne(db)
/// } catch {
/// // By default, sensitive information is NOT printed
/// // when an error occurs:
/// print(error)
/// }
/// ```
///
/// For debugging purpose, you can set this flag to true, and get more
/// precise database reports. It is your responsibility to prevent sensitive
/// information from leaking in unexpected locations, so you should not set
/// this flag in release builds (think about GDPR and other
/// privacy-related rules):
///
/// ```swift
/// var config = Configuration()
/// #if DEBUG
/// // Enable verbose debugging in DEBUG builds only
/// config.publicStatementArguments = true
/// #endif
///
/// db.trace { event in
/// // Sensitive information is printed in DEBUG builds:
/// print(event)
/// }
///
/// do {
/// // The sensitive information to protect
/// let email = "..."
/// let player = try Player.filter(Column("email") == email).fetchOne(db)
/// } catch {
/// // Sensitive information is printed in DEBUG builds:
/// print(error)
/// }
/// ```
public var publicStatementArguments = false
/// The clock that feeds ``Database/transactionDate``.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// The default clock is ``DefaultTransactionClock`` (which returns the
/// start date of the current transaction).
///
/// For example:
///
/// ```swift
/// var config = Configuration()
/// config.transactionClock = .custom { db in /* return some Date */ }
/// ```
public var transactionClock: any TransactionClock = .default
// MARK: - Managing SQLite Connections
private var setups: [(Database) throws -> Void] = []
/// Defines a function to run whenever an SQLite connection is opened.
///
/// The preparation function is run before the connection is made available
/// for database access methods.
///
/// This method can be called several times. The preparation functions are
/// run in the same order.
///
/// For example:
///
/// ```swift
/// var config = Configuration()
/// config.prepareDatabase { db in
/// // Prints all SQL statements
/// db.trace { print("SQL >", $0) }
/// }
/// ```
///
/// When you use a ``DatabasePool``, preparation functions are called for
/// the writer connection and all reader connections. You can distinguish
/// them by querying `db.configuration.readonly`:
///
/// ```swift
/// var config = Configuration()
/// config.prepareDatabase { db in
/// if db.configuration.readonly {
/// // reader connection
/// } else {
/// // writer connection
/// }
/// }
/// ```
///
/// On newly created databases files, ``DatabasePool`` activates the WAL
/// mode after the preparation functions have run.
public mutating func prepareDatabase(_ setup: @escaping (Database) throws -> Void) {
setups.append(setup)
}
// MARK: - Transactions
/// The default kind of write transactions.
///
/// The default is ``Database/TransactionKind/deferred``.
///
/// You can change the default transaction kind. For example, you can force
/// all write transactions to be `IMMEDIATE`:
///
/// ```swift
/// var config = Configuration()
/// config.defaultTransactionKind = .immediate
/// let dbQueue = try DatabaseQueue(configuration: config)
///
/// // BEGIN IMMEDIATE TRANSACTION; ...; COMMIT TRANSACTION;
/// try dbQueue.write { db in ... }
/// ```
///
/// This property is ignored for read-only transactions. Those always open
/// `DEFERRED` SQLite transactions.
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>
public var defaultTransactionKind: Database.TransactionKind = .deferred
/// A boolean value indicating whether it is valid to leave a transaction
/// opened at the end of a database access method.
///
/// The default value is false: not completing a transaction is a
/// programmer error:
///
/// ```swift
/// let dbQueue = try DatabaseQueue()
///
/// // fatal error: A transaction has been left opened at the end of a database access
/// try dbQueue.inDatabase { db in
/// try db.beginTransaction()
/// }
/// ```
///
/// When true, one can leave opened transaction at the end of database
/// access method:
///
/// ```swift
/// var config = Configuration()
/// config.allowsUnsafeTransactions = true
/// let dbQueue = try DatabaseQueue(configuration: config)
///
/// try dbQueue.inDatabase { db in
/// try db.beginTransaction()
/// }
///
/// try dbQueue.inDatabase { db in
/// try db.commit()
/// }
/// ```
///
/// This configuration flag has no effect on ``DatabasePool`` reader
/// connections: those never allow leaving a transaction opened at the end
/// of a read access.
public var allowsUnsafeTransactions = false
// MARK: - Journal Mode
/// Defines how the journal mode is configured when the database
/// connection is opened.
///
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
public enum JournalModeConfiguration: Sendable {
/// The default setup has ``DatabaseQueue`` perform no specific
/// configuration of the journal mode, and ``DatabasePool``
/// configure the database for the WAL mode (just like the
/// ``wal`` case).
case `default`
/// The journal mode is set to WAL (plus extra configurations that
/// make life easier with WAL databases).
case wal
}
/// Defines how the journal mode is configured when the database
/// connection is opened.
///
/// This configuration is ignored when ``readonly`` is true.
///
/// The default value has ``DatabaseQueue`` perform no specific
/// configuration of the journal mode, and ``DatabasePool`` configure
/// the database for the WAL mode.
///
/// Applications that need to open a WAL database with a
/// ``DatabaseQueue`` should set the `journalMode` to `wal`:
///
/// ```swift
/// // Open a WAL database with DatabaseQueue
/// var config = Configuration()
/// config.journalMode = .wal
/// let dbQueue = try DatabaseQueue(path: "...", configuration: config)
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
public var journalMode = JournalModeConfiguration.default
// MARK: - Concurrency
/// Defines the how `SQLITE_BUSY` errors are handled.
///
/// The default is ``Database/BusyMode/immediateError``.
///
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html#busy>
public var busyMode: Database.BusyMode = .immediateError
/// The behavior in case of SQLITE_BUSY error, for read-only connections.
/// If nil, GRDB picks a default one.
var readonlyBusyMode: Database.BusyMode? = nil
/// The maximum number of concurrent reader connections.
///
/// This configuration has effect on ``DatabasePool`` and
/// ``DatabaseSnapshotPool`` only. The default value is 5.
///
/// You can query this value at runtime in order to get the actual capacity
/// for concurrent reads of any ``DatabaseReader``. In this context,
/// ``DatabaseQueue`` and ``DatabaseSnapshot`` have a capacity of 1,
/// because they can't perform two concurrent reads. For example:
///
/// ```swift
/// var config = Configuration()
/// config.maximumReaderCount = 5
///
/// let path = "/path/to/database.sqlite"
/// let dbQueue = try DatabaseQueue(path: path, configuration: config)
/// let dbPool = try DatabasePool(path: path, configuration: config)
/// let dbSnapshot = try dbPool.makeSnapshot()
///
/// print(dbQueue.configuration.maximumReaderCount) // 1
/// print(dbPool.configuration.maximumReaderCount) // 5
/// print(dbSnapshot.configuration.maximumReaderCount) // 1
/// ```
public var maximumReaderCount: Int = 5
/// The quality of service of database accesses.
///
/// The quality of service is ignored if you supply a ``targetQueue``.
///
/// The default is `userInitiated`.
public var qos: DispatchQoS = .userInitiated
/// The effective quality of service of read-only database accesses.
public var readQoS: DispatchQoS {
targetQueue?.qos ?? self.qos
}
/// The effective quality of service of write database accesses.
public var writeQoS: DispatchQoS {
writeTargetQueue?.qos ?? targetQueue?.qos ?? self.qos
}
/// The target dispatch queue for database accesses.
///
/// Database connections which are not read-only will prefer
/// ``writeTargetQueue`` instead, if it is not nil.
///
/// When you use ``DatabasePool``, make sure this queue is concurrent. This
/// is because in a serial dispatch queue, no concurrent database access can
/// happen, and you may experience deadlocks.
///
/// If the queue is nil, all database accesses happen in unspecified
/// dispatch queues whose quality of service is determined by the
/// ``qos`` property.
///
/// The default is nil.
public var targetQueue: DispatchQueue? = nil
/// The target dispatch queue for write database accesses.
///
/// If this queue is nil, writer connections are controlled
/// by ``targetQueue``.
///
/// The default is nil.
public var writeTargetQueue: DispatchQueue? = nil
#if os(iOS)
/// A boolean value indicating whether the database connection releases
/// memory when entering the background or upon receiving a memory warning
/// in iOS.
///
/// The default is true.
public var automaticMemoryManagement = true
#endif
/// A boolean value indicating whether read-only connections should be
/// kept open.
///
/// This configuration flag applies to ``DatabasePool`` only. The
/// default value is false.
///
/// When the flag is false, a `DatabasePool` closes read-only
/// connections when requested to dispose non-essential memory with
/// ``DatabasePool/releaseMemory()``. When true, those connections are
/// kept open.
///
/// Consider setting this flag to true when profiling your application
/// reveals that a lot of time is spent opening new SQLite connections.
public var persistentReadOnlyConnections = false
// MARK: - Factory Configuration
/// Creates a factory configuration.
public init() { }
// MARK: - Not Public
/// The SQLite [threading mode](https://www.sqlite.org/threadsafe.html).
///
/// - Note: Only the multi-thread mode (`SQLITE_OPEN_NOMUTEX`) is currently
/// supported, since all <doc:DatabaseConnections> access SQLite connections
/// through a `SerializedDatabase`.
var threadingMode = Database.ThreadingMode.default
var SQLiteConnectionDidOpen: (() -> Void)?
var SQLiteConnectionWillClose: ((SQLiteConnection) -> Void)?
var SQLiteConnectionDidClose: (() -> Void)?
var SQLiteOpenFlags: CInt {
var flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE)
if sqlite3_libversion_number() >= 3037000 {
flags |= 0x02000000 // SQLITE_OPEN_EXRESCODE
}
return threadingMode.SQLiteOpenFlags | flags
}
func setUp(_ db: Database) throws {
for f in setups {
try f(db)
}
}
func identifier(defaultLabel: String, purpose: String? = nil) -> String {
(self.label ?? defaultLabel) + (purpose.map { "." + $0 } ?? "")
}
/// Creates a DispatchQueue which has the quality of service and target
/// queue of write accesses.
func makeWriterDispatchQueue(label: String) -> DispatchQueue {
if let targetQueue = writeTargetQueue ?? targetQueue {
return DispatchQueue(label: label, target: targetQueue)
} else {
return DispatchQueue(label: label, qos: qos)
}
}
/// Creates a DispatchQueue which has the quality of service and target
/// queue of read accesses.
func makeReaderDispatchQueue(label: String) -> DispatchQueue {
if let targetQueue {
return DispatchQueue(label: label, target: targetQueue)
} else {
return DispatchQueue(label: label, qos: qos)
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,562 @@
import Foundation
extension Database {
// MARK: - Statements
/// Returns a new prepared statement that can be reused.
///
/// For example:
///
/// ```swift
/// let statement = try db.makeStatement(sql: "SELECT * FROM player WHERE id = ?")
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
///
/// let statement = try db.makeStatement(sql: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
/// ```
///
/// - parameter sql: An SQL string.
/// - returns: A prepared statement.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func makeStatement(sql: String) throws -> Statement {
try makeStatement(sql: sql, prepFlags: 0)
}
/// Returns a new prepared statement that can be reused.
///
/// For example:
///
/// ```swift
/// let statement = try db.makeStatement(literal: "SELECT * FROM player WHERE id = ?")
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
///
/// let statement = try db.makeStatement(literal: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
/// ```
///
/// In the provided literal, no argument must be set, or all arguments must
/// be set. An error is raised otherwise. For example:
///
/// ```swift
/// // OK
/// try makeStatement(literal: """
/// SELECT COUNT(*) FROM player WHERE score > ?
/// """)
/// try makeStatement(literal: """
/// SELECT COUNT(*) FROM player WHERE score > \(1000)
/// """)
///
/// // NOT OK (first argument is not set, but second is)
/// try makeStatement(literal: """
/// SELECT COUNT(*) FROM player
/// WHERE color = ? AND score > \(1000)
/// """)
/// ```
///
/// - parameter sqlLiteral: An ``SQL`` literal.
/// - returns: A prepared statement.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func makeStatement(literal sqlLiteral: SQL) throws -> Statement {
let (sql, arguments) = try sqlLiteral.build(self)
let statement = try makeStatement(sql: sql)
if arguments.isEmpty == false {
// Throws if arguments do not match
try statement.setArguments(arguments)
}
return statement
}
/// Returns a new prepared statement that can be reused.
///
/// For example:
///
/// let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?", prepFlags: 0)
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
///
/// - parameter sql: An SQL string.
/// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from
/// SQLite 3.20.0, see <http://www.sqlite.org/c3ref/prepare.html>)
/// - returns: A prepared statement.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
func makeStatement(sql: String, prepFlags: CUnsignedInt) throws -> Statement {
let statements = SQLStatementCursor(database: self, sql: sql, arguments: nil, prepFlags: prepFlags)
guard let statement = try statements.next() else {
throw DatabaseError(
resultCode: .SQLITE_ERROR,
message: "empty statement",
sql: sql)
}
do {
guard try statements.next() == nil else {
throw DatabaseError(
resultCode: .SQLITE_MISUSE,
message: """
Multiple statements found. To execute multiple statements, use \
Database.execute(sql:) or Database.allStatements(sql:) instead.
""",
sql: sql)
}
} catch {
// Something while would not compile was found after the first statement.
// Complain about multiple statements anyway.
throw DatabaseError(
resultCode: .SQLITE_MISUSE,
message: """
Multiple statements found. To execute multiple statements, use \
Database.execute(sql:) or Database.allStatements(sql:) instead.
""",
sql: sql)
}
return statement
}
/// Returns a prepared statement that can be reused.
///
/// For example:
///
/// ```swift
/// let statement = try db.cachedStatement(sql: "SELECT * FROM player WHERE id = ?")
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
///
/// let statement = try db.cachedStatement(sql: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
/// ```
///
/// The returned statement may have already been used: it may or may not
/// contain values for its eventual arguments.
///
/// - parameter sql: An SQL string.
/// - returns: A prepared statement.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func cachedStatement(sql: String) throws -> Statement {
try publicStatementCache.statement(sql)
}
/// Returns a prepared statement that can be reused.
///
/// For example:
///
/// ```swift
/// let statement = try db.cachedStatement(literal: "SELECT * FROM player WHERE id = ?")
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
///
/// let statement = try db.cachedStatement(literal: "INSERT INTO player (name) VALUES (?)")
/// try statement.execute(arguments: ["Arthur"])
/// try statement.execute(arguments: ["Barbara"])
/// ```
///
/// In the provided literal, no argument must be set, or all arguments must
/// be set. An error is raised otherwise. For example:
///
/// ```swift
/// // OK
/// try cachedStatement(literal: """
/// SELECT COUNT(*) FROM player WHERE score > ?
/// """)
/// try cachedStatement(literal: """
/// SELECT COUNT(*) FROM player WHERE score > \(1000)
/// """)
///
/// // NOT OK (first argument is not set, but second is)
/// try cachedStatement(literal: """
/// SELECT COUNT(*) FROM player
/// WHERE color = ? AND score > \(1000)
/// """)
/// ```
///
/// - parameter sqlLiteral: An ``SQL`` literal.
/// - returns: A prepared statement.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func cachedStatement(literal sqlLiteral: SQL) throws -> Statement {
let (sql, arguments) = try sqlLiteral.build(self)
let statement = try cachedStatement(sql: sql)
if arguments.isEmpty == false {
// Throws if arguments do not match
try statement.setArguments(arguments)
}
return statement
}
/// Returns a cached statement that does not conflict with user's cached statements.
func internalCachedStatement(sql: String) throws -> Statement {
try internalStatementCache.statement(sql)
}
/// Returns a cursor of prepared statements.
///
/// For example:
///
/// ```swift
/// let statements = try db.allStatements(sql: """
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
/// while let statement = try statements.next() {
/// try statement.execute()
/// }
/// ```
///
/// The `arguments` parameter must be nil, or all arguments must be set. The
/// returned cursor will throw an error otherwise. For example:
///
/// ```swift
/// // OK
/// try allStatements(sql: """
/// SELECT COUNT(*) FROM player WHERE score < ?;
/// SELECT COUNT(*) FROM player WHERE score > ?;
/// """)
///
/// try allStatements(sql: """
/// SELECT COUNT(*) FROM player WHERE score < ?;
/// SELECT COUNT(*) FROM player WHERE score > ?;
/// """, arguments: [1000, 1000])
///
/// // NOT OK (first argument is set, but second is not)
/// try allStatements(sql: """
/// SELECT COUNT(*) FROM player WHERE score < ?;
/// SELECT COUNT(*) FROM player WHERE score > ?;
/// """, arguments: [1000])
/// ```
///
/// - parameters:
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - returns: A cursor of prepared ``Statement``.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func allStatements(sql: String, arguments: StatementArguments? = nil)
throws -> SQLStatementCursor
{
SQLStatementCursor(database: self, sql: sql, arguments: arguments)
}
/// Returns a cursor of prepared statements.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// let statements = try db.allStatements(literal: """
/// INSERT INTO player (name) VALUES (\("Arthur"));
/// INSERT INTO player (name) VALUES (\("Barbara"));
/// INSERT INTO player (name) VALUES (\("O'Brien"));
/// """)
/// while let statement = try statements.next() {
/// try statement.execute()
/// }
/// ```
///
/// In the provided literal, no argument must be set, or all arguments must
/// be set. The returned cursor will throw an error otherwise. For example:
///
/// ```swift
/// // OK
/// try allStatements(literal: """
/// SELECT COUNT(*) FROM player WHERE score < ?;
/// SELECT COUNT(*) FROM player WHERE score > ?;
/// """)
///
/// try allStatements(literal: """
/// SELECT COUNT(*) FROM player WHERE score < \(1000);
/// SELECT COUNT(*) FROM player WHERE score > \(1000);
/// """)
///
/// // NOT OK (first argument is set, but second is not)
/// try allStatements(literal: """
/// SELECT COUNT(*) FROM player WHERE score < \(1000);
/// SELECT COUNT(*) FROM player WHERE score > ?;
/// """)
/// ```
///
/// - parameter sqlLiteral: An ``SQL`` literal.
/// - returns: A cursor of prepared ``Statement``.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func allStatements(literal sqlLiteral: SQL) throws -> SQLStatementCursor {
let context = SQLGenerationContext(self)
let sql = try sqlLiteral.sql(context)
let arguments = context.arguments.isEmpty
? nil // builds statements without arguments
: context.arguments // force arguments to match
return SQLStatementCursor(database: self, sql: sql, arguments: arguments)
}
/// Executes one or several SQL statements.
///
/// For example:
///
/// ```swift
/// try db.execute(
/// sql: "INSERT INTO player (name) VALUES (:name)",
/// arguments: ["name": "Arthur"])
///
/// try db.execute(sql: """
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// INSERT INTO player (name) VALUES (?);
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
/// ```
///
/// - parameters:
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func execute(sql: String, arguments: StatementArguments = StatementArguments()) throws {
try execute(literal: SQL(sql: sql, arguments: arguments))
}
/// Executes one or several SQL statements.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// try db.execute(literal: """
/// INSERT INTO player (name) VALUES (\("Arthur"))
/// """)
///
/// try db.execute(literal: """
/// INSERT INTO player (name) VALUES (\("Arthur"));
/// INSERT INTO player (name) VALUES (\("Barbara"));
/// INSERT INTO player (name) VALUES (\("O'Brien"));
/// """)
/// ```
///
/// - parameter sqlLiteral: An ``SQL`` literal.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func execute(literal sqlLiteral: SQL) throws {
let statements = try allStatements(literal: sqlLiteral)
while let statement = try statements.next() {
try statement.execute()
}
}
}
/// A cursor over all statements in an SQL string.
public class SQLStatementCursor {
private let database: Database
private let cString: ContiguousArray<CChar>
private let prepFlags: CUnsignedInt
private let initialArgumentCount: Int?
// Mutated by iteration
private var offset: Int // offset in the C string
private var arguments: StatementArguments? // Nil when arguments are set later
init(database: Database, sql: String, arguments: StatementArguments?, prepFlags: CUnsignedInt = 0) {
self.database = database
self.cString = sql.utf8CString
self.prepFlags = prepFlags
self.initialArgumentCount = arguments?.values.count
self.offset = 0
self.arguments = arguments
}
}
// Explicit non-conformance to Sendable: database cursors must be used from
// a serialized database access dispatch queue.
@available(*, unavailable)
extension SQLStatementCursor: Sendable { }
extension SQLStatementCursor: Cursor {
public func next() throws -> Statement? {
guard offset < cString.count - 1 /* trailing \0 */ else {
// End of C string -> end of cursor.
try checkArgumentsAreEmpty()
return nil
}
return try cString.withUnsafeBufferPointer { buffer in
let baseAddress = buffer.baseAddress! // never nil because the buffer contains the trailing \0.
// Compile next statement
var statementEnd: UnsafePointer<CChar>? = nil
let statement = try Statement(
database: database,
statementStart: baseAddress + offset,
statementEnd: &statementEnd,
prepFlags: prepFlags)
// Advance to next statement
offset = statementEnd! - baseAddress // never nil because statement compilation did not fail.
guard let statement else {
// No statement found -> end of cursor.
try checkArgumentsAreEmpty()
return nil
}
if arguments != nil {
// Extract statement arguments
let bindings = try arguments!.extractBindings(
forStatement: statement,
allowingRemainingValues: true)
// unchecked is OK because we just extracted the correct
// number of arguments
statement.setUncheckedArguments(StatementArguments(bindings))
}
return statement
}
}
/// Check that all arguments were consumed: it is a programmer error to
/// provide arguments that do not match the statements.
private func checkArgumentsAreEmpty() throws {
if let arguments,
let initialArgumentCount,
arguments.values.isEmpty == false
{
throw DatabaseError(
resultCode: .SQLITE_MISUSE,
message: "wrong number of statement arguments: \(initialArgumentCount)")
}
}
}
extension Database {
/// Makes sure statement can be executed, and prepares database observation.
@usableFromInline
func statementWillExecute(_ statement: Statement) throws {
// Aborted transactions prevent statement execution (see the
// documentation of this method for more information).
try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments)
// Suspended databases must not execute statements that create the risk
// of `0xdead10cc` exception (see the documentation of this method for
// more information).
try checkForSuspensionViolation(from: statement)
// Record the database region selected by the statement execution.
try registerAccess(to: statement.databaseRegion)
// Database observation: prepare transaction observers.
observationBroker?.statementWillExecute(statement)
}
/// May throw a cancelled commit error, if a transaction observer cancels
/// an empty transaction.
@usableFromInline
func statementDidExecute(_ statement: Statement) throws {
if statement.invalidatesDatabaseSchemaCache {
clearSchemaCache()
}
checkForAutocommitTransition()
// Database observation: cleanup
try observationBroker?.statementDidExecute(statement)
}
/// Always throws an error
@usableFromInline
func statementDidFail(_ statement: Statement, withResultCode resultCode: CInt) throws -> Never {
// Failed statements can not be reused, because `sqlite3_reset` won't
// be able to restore the statement to its initial state:
// https://www.sqlite.org/c3ref/reset.html
//
// So make sure we clear this statement from the cache.
internalStatementCache.remove(statement)
publicStatementCache.remove(statement)
checkForAutocommitTransition()
// Extract values that may be modified by the user in their
// `TransactionObserver.databaseDidRollback(_:)` implementation
// (see below).
let message = lastErrorMessage
let arguments = statement.arguments
// Database observation: cleanup.
//
// If the statement failure is due to a transaction observer that has
// cancelled a transaction, this calls `TransactionObserver.databaseDidRollback(_:)`,
// and throws the user-provided cancelled commit error.
try observationBroker?.statementDidFail(statement)
// Throw statement failure
throw DatabaseError(
resultCode: resultCode,
message: message,
sql: statement.sql,
arguments: arguments,
publicStatementArguments: configuration.publicStatementArguments)
}
private func checkForAutocommitTransition() {
if sqlite3_get_autocommit(sqliteConnection) == 0 {
if autocommitState == .on {
// Record transaction date as soon as the connection leaves
// auto-commit mode.
// We grab a result, so that this failure is later reported
// whenever the user calls `Database.transactionDate`.
transactionDateResult = Result { try configuration.transactionClock.now(self) }
}
autocommitState = .off
} else {
if autocommitState == .off {
// Reset transaction date
transactionDateResult = nil
}
autocommitState = .on
}
}
}
/// A thread-unsafe statement cache
struct StatementCache {
unowned let db: Database
private var statements: [String: Statement] = [:]
init(database: Database) {
self.db = database
}
mutating func statement(_ sql: String) throws -> Statement {
if let statement = statements[sql] {
return statement
}
// http://www.sqlite.org/c3ref/c_prepare_persistent.html#sqlitepreparepersistent
// > The SQLITE_PREPARE_PERSISTENT flag is a hint to the query
// > planner that the prepared statement will be retained for a long
// > time and probably reused many times.
//
// This looks like a perfect match for cached statements.
//
// However SQLITE_PREPARE_PERSISTENT was only introduced in
// SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20
#if GRDBCUSTOMSQLITE || GRDBCIPHER
let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT))
#else
let statement: Statement
if #available(iOS 12, macOS 10.14, watchOS 5, *) { // SQLite 3.20+
statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT))
} else {
statement = try db.makeStatement(sql: sql)
}
#endif
statements[sql] = statement
return statement
}
mutating func clear() {
statements = [:]
}
mutating func remove(_ statement: Statement) {
statements.removeFirst { $0.value === statement }
}
mutating func removeAll(where shouldBeRemoved: (Statement) -> Bool) {
statements = statements.filter { (_, statement) in !shouldBeRemoved(statement) }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
/// Describe the progress of a database backup.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/backup_finish.html>
public struct DatabaseBackupProgress: Sendable {
/// The number of pages still to be backed up.
///
/// It is the result of the `sqlite3_backup_remaining` function.
public let remainingPageCount: Int
/// The number of pages in the source database.
///
/// It is the result of the `sqlite3_backup_pagecount` function.
public let totalPageCount: Int
/// The number of of backed up pages.
///
/// It is equal to `totalPageCount - remainingPageCount`.
public var completedPageCount: Int {
totalPageCount - remainingPageCount
}
/// A boolean value indicating whether the backup is complete.
///
/// It is true if and only if the last call the `sqlite3_backup_step` has
/// returned `SQLITE_DONE`.
public let isCompleted: Bool
}
@@ -0,0 +1,78 @@
import Foundation
/// `DatabaseCollation` is a custom string comparison function used by SQLite.
///
/// See also ``Database/CollationName``.
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#collating_sequences>
///
/// ## Topics
///
/// ### Creating a Custom Collation
///
/// - ``init(_:function:)``
/// - ``name``
///
/// ### Built-in Collations
///
/// - ``caseInsensitiveCompare``
/// - ``localizedCaseInsensitiveCompare``
/// - ``localizedCompare``
/// - ``localizedStandardCompare``
/// - ``unicodeCompare``
public final class DatabaseCollation {
/// The name of the collation.
public let name: String
let function: (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult
/// Creates a collation.
///
/// For example:
///
/// ```swift
/// let collation = DatabaseCollation("localized_standard") { (string1, string2) in
/// return (string1 as NSString).localizedStandardCompare(string2)
/// }
/// db.add(collation: collation)
/// try db.execute(sql: "CREATE TABLE file (name TEXT COLLATE localized_standard")
/// ```
///
/// - parameters:
/// - name: The collation name.
/// - function: A function that compares two strings.
public init(_ name: String, function: @escaping (String, String) -> ComparisonResult) {
self.name = name
self.function = { (length1, buffer1, length2, buffer2) in
// Buffers are not C strings: they do not end with \0.
let string1 = String(
bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer1.unsafelyUnwrapped),
length: Int(length1),
encoding: .utf8,
freeWhenDone: false)!
let string2 = String(
bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer2.unsafelyUnwrapped),
length: Int(length2),
encoding: .utf8,
freeWhenDone: false)!
return function(string1, string2)
}
}
}
extension DatabaseCollation: Hashable {
// Collation equality is based on the sqlite3_strnicmp SQLite function.
// (see https://www.sqlite.org/c3ref/create_collation.html). Computing
// a hash value that honors the Swift Hashable contract (value equality
// implies hash equality) is thus non trivial. But it's not that
// important, since this hashValue is only used when one adds
// or removes a collation from a database connection.
public func hash(into hasher: inout Hasher) {
hasher.combine(0)
}
/// Two collations are equal if they share the same name (case insensitive)
public static func == (lhs: DatabaseCollation, rhs: DatabaseCollation) -> Bool {
// See <https://www.sqlite.org/c3ref/create_collation.html>
return sqlite3_stricmp(lhs.name, rhs.name) == 0
}
}
@@ -0,0 +1,612 @@
import Foundation
/// An SQLite result code.
///
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
public struct ResultCode: RawRepresentable, Equatable {
/// The raw SQLite result code.
public let rawValue: CInt
/// Creates a `ResultCode` from a raw SQLite result code.
public init(rawValue: CInt) {
self.rawValue = rawValue
}
/// A primary result code limited to the least significant 8 bits.
///
/// For example:
///
/// ```swift
/// let resultCode = .SQLITE_CONSTRAINT_FOREIGNKEY
/// resultCode.primaryResultCode == .SQLITE_CONSTRAINT // true
/// ```
public var primaryResultCode: ResultCode {
ResultCode(rawValue: rawValue & 0xFF)
}
var isPrimary: Bool { self == primaryResultCode }
/// Returns true if the code on the left matches the code on the right.
///
/// Primary result codes match themselves and their extended result codes,
/// while extended result codes match only themselves:
///
/// For example:
///
/// ```swift
/// switch error.extendedResultCode {
/// case .SQLITE_CONSTRAINT_FOREIGNKEY: // foreign key constraint error
/// case .SQLITE_CONSTRAINT: // any other constraint error
/// default: // any other database error
/// }
/// ```
public static func ~= (pattern: ResultCode, code: ResultCode) -> Bool {
if pattern.isPrimary {
return pattern == code.primaryResultCode
} else {
return pattern == code
}
}
// Primary Result codes
// https://www.sqlite.org/rescode.html#primary_result_code_list
// swiftlint:disable operator_usage_whitespace
public static let SQLITE_OK = ResultCode(rawValue: 0) // Successful result
public static let SQLITE_ERROR = ResultCode(rawValue: 1) // SQL error or missing database
public static let SQLITE_INTERNAL = ResultCode(rawValue: 2) // Internal logic error in SQLite
public static let SQLITE_PERM = ResultCode(rawValue: 3) // Access permission denied
public static let SQLITE_ABORT = ResultCode(rawValue: 4) // Callback routine requested an abort
public static let SQLITE_BUSY = ResultCode(rawValue: 5) // The database file is locked
public static let SQLITE_LOCKED = ResultCode(rawValue: 6) // A table in the database is locked
public static let SQLITE_NOMEM = ResultCode(rawValue: 7) // A malloc() failed
public static let SQLITE_READONLY = ResultCode(rawValue: 8) // Attempt to write a readonly database
public static let SQLITE_INTERRUPT = ResultCode(rawValue: 9) // Operation terminated by sqlite3_interrupt()
public static let SQLITE_IOERR = ResultCode(rawValue: 10) // Some kind of disk I/O error occurred
public static let SQLITE_CORRUPT = ResultCode(rawValue: 11) // The database disk image is malformed
public static let SQLITE_NOTFOUND = ResultCode(rawValue: 12) // Unknown opcode in sqlite3_file_control()
public static let SQLITE_FULL = ResultCode(rawValue: 13) // Insertion failed because database is full
public static let SQLITE_CANTOPEN = ResultCode(rawValue: 14) // Unable to open the database file
public static let SQLITE_PROTOCOL = ResultCode(rawValue: 15) // Database lock protocol error
public static let SQLITE_EMPTY = ResultCode(rawValue: 16) // Database is empty
public static let SQLITE_SCHEMA = ResultCode(rawValue: 17) // The database schema changed
public static let SQLITE_TOOBIG = ResultCode(rawValue: 18) // String or BLOB exceeds size limit
public static let SQLITE_CONSTRAINT = ResultCode(rawValue: 19) // Abort due to constraint violation
public static let SQLITE_MISMATCH = ResultCode(rawValue: 20) // Data type mismatch
public static let SQLITE_MISUSE = ResultCode(rawValue: 21) // Library used incorrectly
public static let SQLITE_NOLFS = ResultCode(rawValue: 22) // Uses OS features not supported on host
public static let SQLITE_AUTH = ResultCode(rawValue: 23) // Authorization denied
public static let SQLITE_FORMAT = ResultCode(rawValue: 24) // Auxiliary database format error
public static let SQLITE_RANGE = ResultCode(rawValue: 25) // 2nd parameter to sqlite3_bind out of range
public static let SQLITE_NOTADB = ResultCode(rawValue: 26) // File opened that is not a database file
public static let SQLITE_NOTICE = ResultCode(rawValue: 27) // Notifications from sqlite3_log()
public static let SQLITE_WARNING = ResultCode(rawValue: 28) // Warnings from sqlite3_log()
public static let SQLITE_ROW = ResultCode(rawValue: 100) // sqlite3_step() has another row ready
public static let SQLITE_DONE = ResultCode(rawValue: 101) // sqlite3_step() has finished executing
// swiftlint:enable operator_usage_whitespace
// Extended Result Code
// https://www.sqlite.org/rescode.html#extended_result_code_list
// swiftlint:disable operator_usage_whitespace line_length
public static let SQLITE_ERROR_MISSING_COLLSEQ = ResultCode(rawValue: (SQLITE_ERROR.rawValue | (1<<8)))
public static let SQLITE_ERROR_RETRY = ResultCode(rawValue: (SQLITE_ERROR.rawValue | (2<<8)))
public static let SQLITE_ERROR_SNAPSHOT = ResultCode(rawValue: (SQLITE_ERROR.rawValue | (3<<8)))
public static let SQLITE_IOERR_READ = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (1<<8)))
public static let SQLITE_IOERR_SHORT_READ = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (2<<8)))
public static let SQLITE_IOERR_WRITE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (3<<8)))
public static let SQLITE_IOERR_FSYNC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (4<<8)))
public static let SQLITE_IOERR_DIR_FSYNC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (5<<8)))
public static let SQLITE_IOERR_TRUNCATE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (6<<8)))
public static let SQLITE_IOERR_FSTAT = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (7<<8)))
public static let SQLITE_IOERR_UNLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (8<<8)))
public static let SQLITE_IOERR_RDLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (9<<8)))
public static let SQLITE_IOERR_DELETE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (10<<8)))
public static let SQLITE_IOERR_BLOCKED = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (11<<8)))
public static let SQLITE_IOERR_NOMEM = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (12<<8)))
public static let SQLITE_IOERR_ACCESS = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (13<<8)))
public static let SQLITE_IOERR_CHECKRESERVEDLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (14<<8)))
public static let SQLITE_IOERR_LOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (15<<8)))
public static let SQLITE_IOERR_CLOSE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (16<<8)))
public static let SQLITE_IOERR_DIR_CLOSE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (17<<8)))
public static let SQLITE_IOERR_SHMOPEN = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (18<<8)))
public static let SQLITE_IOERR_SHMSIZE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (19<<8)))
public static let SQLITE_IOERR_SHMLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (20<<8)))
public static let SQLITE_IOERR_SHMMAP = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (21<<8)))
public static let SQLITE_IOERR_SEEK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (22<<8)))
public static let SQLITE_IOERR_DELETE_NOENT = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (23<<8)))
public static let SQLITE_IOERR_MMAP = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (24<<8)))
public static let SQLITE_IOERR_GETTEMPPATH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (25<<8)))
public static let SQLITE_IOERR_CONVPATH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (26<<8)))
public static let SQLITE_IOERR_VNODE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (27<<8)))
public static let SQLITE_IOERR_AUTH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (28<<8)))
public static let SQLITE_IOERR_BEGIN_ATOMIC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (29<<8)))
public static let SQLITE_IOERR_COMMIT_ATOMIC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (30<<8)))
public static let SQLITE_IOERR_ROLLBACK_ATOMIC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (31<<8)))
public static let SQLITE_IOERR_DATA = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (32<<8)))
public static let SQLITE_IOERR_CORRUPTFS = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (33<<8)))
public static let SQLITE_LOCKED_SHAREDCACHE = ResultCode(rawValue: (SQLITE_LOCKED.rawValue | (1<<8)))
public static let SQLITE_LOCKED_VTAB = ResultCode(rawValue: (SQLITE_LOCKED.rawValue | (2<<8)))
public static let SQLITE_BUSY_RECOVERY = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (1<<8)))
public static let SQLITE_BUSY_SNAPSHOT = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (2<<8)))
public static let SQLITE_BUSY_TIMEOUT = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (3<<8)))
public static let SQLITE_CANTOPEN_NOTEMPDIR = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (1<<8)))
public static let SQLITE_CANTOPEN_ISDIR = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (2<<8)))
public static let SQLITE_CANTOPEN_FULLPATH = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (3<<8)))
public static let SQLITE_CANTOPEN_CONVPATH = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (4<<8)))
public static let SQLITE_CANTOPEN_DIRTYWAL = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (5<<8))) /* Not Used */
public static let SQLITE_CANTOPEN_SYMLINK = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (6<<8)))
public static let SQLITE_CORRUPT_VTAB = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (1<<8)))
public static let SQLITE_CORRUPT_SEQUENCE = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (2<<8)))
public static let SQLITE_CORRUPT_INDEX = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (3<<8)))
public static let SQLITE_READONLY_RECOVERY = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (1<<8)))
public static let SQLITE_READONLY_CANTLOCK = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (2<<8)))
public static let SQLITE_READONLY_ROLLBACK = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (3<<8)))
public static let SQLITE_READONLY_DBMOVED = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (4<<8)))
public static let SQLITE_READONLY_CANTINIT = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (5<<8)))
public static let SQLITE_READONLY_DIRECTORY = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (6<<8)))
public static let SQLITE_ABORT_ROLLBACK = ResultCode(rawValue: (SQLITE_ABORT.rawValue | (2<<8)))
public static let SQLITE_CONSTRAINT_CHECK = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (1<<8)))
public static let SQLITE_CONSTRAINT_COMMITHOOK = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (2<<8)))
public static let SQLITE_CONSTRAINT_FOREIGNKEY = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (3<<8)))
public static let SQLITE_CONSTRAINT_FUNCTION = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (4<<8)))
public static let SQLITE_CONSTRAINT_NOTNULL = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (5<<8)))
public static let SQLITE_CONSTRAINT_PRIMARYKEY = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (6<<8)))
public static let SQLITE_CONSTRAINT_TRIGGER = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (7<<8)))
public static let SQLITE_CONSTRAINT_UNIQUE = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (8<<8)))
public static let SQLITE_CONSTRAINT_VTAB = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (9<<8)))
public static let SQLITE_CONSTRAINT_ROWID = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (10<<8)))
public static let SQLITE_CONSTRAINT_PINNED = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (11<<8)))
public static let SQLITE_CONSTRAINT_DATATYPE = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (12<<8)))
public static let SQLITE_NOTICE_RECOVER_WAL = ResultCode(rawValue: (SQLITE_NOTICE.rawValue | (1<<8)))
public static let SQLITE_NOTICE_RECOVER_ROLLBACK = ResultCode(rawValue: (SQLITE_NOTICE.rawValue | (2<<8)))
public static let SQLITE_WARNING_AUTOINDEX = ResultCode(rawValue: (SQLITE_WARNING.rawValue | (1<<8)))
public static let SQLITE_AUTH_USER = ResultCode(rawValue: (SQLITE_AUTH.rawValue | (1<<8)))
public static let SQLITE_OK_LOAD_PERMANENTLY = ResultCode(rawValue: (SQLITE_OK.rawValue | (1<<8)))
public static let SQLITE_OK_SYMLINK = ResultCode(rawValue: (SQLITE_OK.rawValue | (2<<8)))
// swiftlint:enable operator_usage_whitespace line_length
}
extension ResultCode {
/// Returns true if the code on the left matches the error on the right.
///
/// Primary result codes match themselves and their extended result codes,
/// while extended result codes match only themselves.
///
/// For example:
///
/// ```swift
/// do {
/// try ...
/// } catch ResultCode.SQLITE_CONSTRAINT_FOREIGNKEY {
/// // foreign key constraint error
/// } catch ResultCode.SQLITE_CONSTRAINT {
/// // any other constraint error
/// } catch {
/// // any other database error
/// }
/// ```
public static func ~= (lhs: Self, rhs: Error) -> Bool {
guard let error = rhs as? DatabaseError else { return false }
return lhs ~= error.extendedResultCode
}
}
extension ResultCode: CustomStringConvertible {
var errorString: String? {
String(cString: sqlite3_errstr(rawValue))
}
public var description: String {
if let errorString {
return "\(rawValue) (\(errorString))"
} else {
return "\(rawValue)"
}
}
}
extension ResultCode: Sendable { }
/// A `DatabaseError` describes an SQLite error.
///
/// For example:
///
/// ```swift
/// do {
/// try player.insert(db)
/// } catch let error as DatabaseError {
/// print(error) // prints debugging information
///
/// switch error {
/// case DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY:
/// // foreign key constraint error
/// case DatabaseError.SQLITE_CONSTRAINT:
/// // any other constraint error
/// default:
/// // any other database error
/// }
/// }
/// ```
///
/// The above example can also be written in a shorter way:
///
/// ```swift
/// do {
/// try player.insert(db)
/// } catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY {
/// // foreign key constraint error
/// } catch DatabaseError.SQLITE_CONSTRAINT {
/// // any other constraint error
/// } catch {
/// // any other database error
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
///
/// ## Topics
///
/// ### Creating DatabaseError
///
/// - ``init(resultCode:message:sql:arguments:publicStatementArguments:)``
/// - ``ResultCode``
///
/// ### Error Information
///
/// - ``arguments``
/// - ``extendedResultCode``
/// - ``isInterruptionError``
/// - ``message``
/// - ``resultCode``
/// - ``sql``
///
/// ### Converting DatabaseError to String
///
/// - ``description``
/// - ``expandedDescription``
public struct DatabaseError: Error {
/// The SQLite primary result code.
///
/// This property returns a "primary result code", that is to say the least
/// significant 8 bits of any SQLite result code.
///
/// For example:
///
/// ```swift
/// do {
/// ...
/// } catch let error as DatabaseError where error.resultCode == .SQL_CONSTRAINT {
/// // A constraint error
/// }
/// ```
///
/// The above example can also be written in a shorter way:
///
/// ```swift
/// do {
/// ...
/// } catch DatabaseError.SQL_CONSTRAINT {
/// // A constraint error
/// }
/// ```
///
/// See also ``extendedResultCode``.
///
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
public var resultCode: ResultCode {
extendedResultCode.primaryResultCode
}
/// The SQLite extended error code.
///
/// For example:
///
/// ```swift
/// do {
/// ...
/// } catch let error as DatabaseError where error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY {
/// // A foreign key constraint error
/// }
/// ```
///
/// The above example can also be written in a shorter way:
///
/// ```swift
/// do {
/// ...
/// } catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY {
/// // A foreign key constraint error
/// }
/// ```
///
/// See also ``resultCode``.
///
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
public let extendedResultCode: ResultCode
/// The SQLite error message.
public let message: String?
/// The SQL query that yielded the error.
public let sql: String?
/// The query arguments that yielded the error.
public let arguments: StatementArguments?
/// See Configuration.publicStatementArguments
var publicStatementArguments: Bool
/// Creates a `DatabaseError`.
///
/// - parameters:
/// - resultCode: A ResultCode (defaults to .SQLITE_ERROR).
/// - message: An eventual error message. If nil, the error message is
/// derived from the result code.
/// - sql: An eventual SQL string.
/// - arguments: Eventual statement arguments.
/// - publicStatementArguments: If false (the default), statement
/// arguments are not visible in the error's ``description`` property.
public init(
resultCode: ResultCode = .SQLITE_ERROR,
message: String? = nil,
sql: String? = nil,
arguments: StatementArguments? = nil,
publicStatementArguments: Bool = false)
{
self.extendedResultCode = resultCode
self.message = message ?? resultCode.errorString
self.sql = sql
self.arguments = arguments
self.publicStatementArguments = publicStatementArguments
}
/// Creates a Database Error with a raw CInt result code.
///
/// This initializer is not public because library user is not supposed to
/// be exposed to raw result codes.
@usableFromInline
init(resultCode: CInt, message: String? = nil, sql: String? = nil) {
self.init(
resultCode: ResultCode(rawValue: resultCode),
message: message,
sql: sql)
}
/// Creates a Database Error with a raw CInt result code.
///
/// This initializer is not public because library user is not supposed to
/// be exposed to raw result codes.
@usableFromInline
init(
resultCode: CInt,
message: String? = nil,
sql: String? = nil,
arguments: StatementArguments?,
publicStatementArguments: Bool)
{
self.init(
resultCode: ResultCode(rawValue: resultCode),
message: message,
sql: sql,
arguments: arguments,
publicStatementArguments: publicStatementArguments)
}
static func noSuchTable(_ tableName: String) -> Self {
DatabaseError(message: "no such table: \(tableName)")
}
static func noSuchSchema(_ schemaName: String) -> Self {
DatabaseError(message: "no such schema: \(schemaName)")
}
}
extension DatabaseError {
static func connectionIsClosed() -> Self {
DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed")
}
}
// Support for `catch DatabaseError.SQLITE_XXX`
extension DatabaseError {
public static let SQLITE_OK = ResultCode.SQLITE_OK
public static let SQLITE_ERROR = ResultCode.SQLITE_ERROR
public static let SQLITE_INTERNAL = ResultCode.SQLITE_INTERNAL
public static let SQLITE_PERM = ResultCode.SQLITE_PERM
public static let SQLITE_ABORT = ResultCode.SQLITE_ABORT
public static let SQLITE_BUSY = ResultCode.SQLITE_BUSY
public static let SQLITE_LOCKED = ResultCode.SQLITE_LOCKED
public static let SQLITE_NOMEM = ResultCode.SQLITE_NOMEM
public static let SQLITE_READONLY = ResultCode.SQLITE_READONLY
public static let SQLITE_INTERRUPT = ResultCode.SQLITE_INTERRUPT
public static let SQLITE_IOERR = ResultCode.SQLITE_IOERR
public static let SQLITE_CORRUPT = ResultCode.SQLITE_CORRUPT
public static let SQLITE_NOTFOUND = ResultCode.SQLITE_NOTFOUND
public static let SQLITE_FULL = ResultCode.SQLITE_FULL
public static let SQLITE_CANTOPEN = ResultCode.SQLITE_CANTOPEN
public static let SQLITE_PROTOCOL = ResultCode.SQLITE_PROTOCOL
public static let SQLITE_EMPTY = ResultCode.SQLITE_EMPTY
public static let SQLITE_SCHEMA = ResultCode.SQLITE_SCHEMA
public static let SQLITE_TOOBIG = ResultCode.SQLITE_TOOBIG
public static let SQLITE_CONSTRAINT = ResultCode.SQLITE_CONSTRAINT
public static let SQLITE_MISMATCH = ResultCode.SQLITE_MISMATCH
public static let SQLITE_MISUSE = ResultCode.SQLITE_MISUSE
public static let SQLITE_NOLFS = ResultCode.SQLITE_NOLFS
public static let SQLITE_AUTH = ResultCode.SQLITE_AUTH
public static let SQLITE_FORMAT = ResultCode.SQLITE_FORMAT
public static let SQLITE_RANGE = ResultCode.SQLITE_RANGE
public static let SQLITE_NOTADB = ResultCode.SQLITE_NOTADB
public static let SQLITE_NOTICE = ResultCode.SQLITE_NOTICE
public static let SQLITE_WARNING = ResultCode.SQLITE_WARNING
public static let SQLITE_ROW = ResultCode.SQLITE_ROW
public static let SQLITE_DONE = ResultCode.SQLITE_DONE
public static let SQLITE_ERROR_MISSING_COLLSEQ = ResultCode.SQLITE_ERROR_MISSING_COLLSEQ
public static let SQLITE_ERROR_RETRY = ResultCode.SQLITE_ERROR_RETRY
public static let SQLITE_ERROR_SNAPSHOT = ResultCode.SQLITE_ERROR_SNAPSHOT
public static let SQLITE_IOERR_READ = ResultCode.SQLITE_IOERR_READ
public static let SQLITE_IOERR_SHORT_READ = ResultCode.SQLITE_IOERR_SHORT_READ
public static let SQLITE_IOERR_WRITE = ResultCode.SQLITE_IOERR_WRITE
public static let SQLITE_IOERR_FSYNC = ResultCode.SQLITE_IOERR_FSYNC
public static let SQLITE_IOERR_DIR_FSYNC = ResultCode.SQLITE_IOERR_DIR_FSYNC
public static let SQLITE_IOERR_TRUNCATE = ResultCode.SQLITE_IOERR_TRUNCATE
public static let SQLITE_IOERR_FSTAT = ResultCode.SQLITE_IOERR_FSTAT
public static let SQLITE_IOERR_UNLOCK = ResultCode.SQLITE_IOERR_UNLOCK
public static let SQLITE_IOERR_RDLOCK = ResultCode.SQLITE_IOERR_RDLOCK
public static let SQLITE_IOERR_DELETE = ResultCode.SQLITE_IOERR_DELETE
public static let SQLITE_IOERR_BLOCKED = ResultCode.SQLITE_IOERR_BLOCKED
public static let SQLITE_IOERR_NOMEM = ResultCode.SQLITE_IOERR_NOMEM
public static let SQLITE_IOERR_ACCESS = ResultCode.SQLITE_IOERR_ACCESS
public static let SQLITE_IOERR_CHECKRESERVEDLOCK = ResultCode.SQLITE_IOERR_CHECKRESERVEDLOCK
public static let SQLITE_IOERR_LOCK = ResultCode.SQLITE_IOERR_LOCK
public static let SQLITE_IOERR_CLOSE = ResultCode.SQLITE_IOERR_CLOSE
public static let SQLITE_IOERR_DIR_CLOSE = ResultCode.SQLITE_IOERR_DIR_CLOSE
public static let SQLITE_IOERR_SHMOPEN = ResultCode.SQLITE_IOERR_SHMOPEN
public static let SQLITE_IOERR_SHMSIZE = ResultCode.SQLITE_IOERR_SHMSIZE
public static let SQLITE_IOERR_SHMLOCK = ResultCode.SQLITE_IOERR_SHMLOCK
public static let SQLITE_IOERR_SHMMAP = ResultCode.SQLITE_IOERR_SHMMAP
public static let SQLITE_IOERR_SEEK = ResultCode.SQLITE_IOERR_SEEK
public static let SQLITE_IOERR_DELETE_NOENT = ResultCode.SQLITE_IOERR_DELETE_NOENT
public static let SQLITE_IOERR_MMAP = ResultCode.SQLITE_IOERR_MMAP
public static let SQLITE_IOERR_GETTEMPPATH = ResultCode.SQLITE_IOERR_GETTEMPPATH
public static let SQLITE_IOERR_CONVPATH = ResultCode.SQLITE_IOERR_CONVPATH
public static let SQLITE_IOERR_VNODE = ResultCode.SQLITE_IOERR_VNODE
public static let SQLITE_IOERR_AUTH = ResultCode.SQLITE_IOERR_AUTH
public static let SQLITE_IOERR_BEGIN_ATOMIC = ResultCode.SQLITE_IOERR_BEGIN_ATOMIC
public static let SQLITE_IOERR_COMMIT_ATOMIC = ResultCode.SQLITE_IOERR_COMMIT_ATOMIC
public static let SQLITE_IOERR_ROLLBACK_ATOMIC = ResultCode.SQLITE_IOERR_ROLLBACK_ATOMIC
public static let SQLITE_IOERR_DATA = ResultCode.SQLITE_IOERR_DATA
public static let SQLITE_IOERR_CORRUPTFS = ResultCode.SQLITE_IOERR_CORRUPTFS
public static let SQLITE_LOCKED_SHAREDCACHE = ResultCode.SQLITE_LOCKED_SHAREDCACHE
public static let SQLITE_LOCKED_VTAB = ResultCode.SQLITE_LOCKED_VTAB
public static let SQLITE_BUSY_RECOVERY = ResultCode.SQLITE_BUSY_RECOVERY
public static let SQLITE_BUSY_SNAPSHOT = ResultCode.SQLITE_BUSY_SNAPSHOT
public static let SQLITE_BUSY_TIMEOUT = ResultCode.SQLITE_BUSY_TIMEOUT
public static let SQLITE_CANTOPEN_NOTEMPDIR = ResultCode.SQLITE_CANTOPEN_NOTEMPDIR
public static let SQLITE_CANTOPEN_ISDIR = ResultCode.SQLITE_CANTOPEN_ISDIR
public static let SQLITE_CANTOPEN_FULLPATH = ResultCode.SQLITE_CANTOPEN_FULLPATH
public static let SQLITE_CANTOPEN_CONVPATH = ResultCode.SQLITE_CANTOPEN_CONVPATH
public static let SQLITE_CANTOPEN_DIRTYWAL = ResultCode.SQLITE_CANTOPEN_DIRTYWAL
public static let SQLITE_CANTOPEN_SYMLINK = ResultCode.SQLITE_CANTOPEN_SYMLINK
public static let SQLITE_CORRUPT_VTAB = ResultCode.SQLITE_CORRUPT_VTAB
public static let SQLITE_CORRUPT_SEQUENCE = ResultCode.SQLITE_CORRUPT_SEQUENCE
public static let SQLITE_CORRUPT_INDEX = ResultCode.SQLITE_CORRUPT_INDEX
public static let SQLITE_READONLY_RECOVERY = ResultCode.SQLITE_READONLY_RECOVERY
public static let SQLITE_READONLY_CANTLOCK = ResultCode.SQLITE_READONLY_CANTLOCK
public static let SQLITE_READONLY_ROLLBACK = ResultCode.SQLITE_READONLY_ROLLBACK
public static let SQLITE_READONLY_DBMOVED = ResultCode.SQLITE_READONLY_DBMOVED
public static let SQLITE_READONLY_CANTINIT = ResultCode.SQLITE_READONLY_CANTINIT
public static let SQLITE_READONLY_DIRECTORY = ResultCode.SQLITE_READONLY_DIRECTORY
public static let SQLITE_ABORT_ROLLBACK = ResultCode.SQLITE_ABORT_ROLLBACK
public static let SQLITE_CONSTRAINT_CHECK = ResultCode.SQLITE_CONSTRAINT_CHECK
public static let SQLITE_CONSTRAINT_COMMITHOOK = ResultCode.SQLITE_CONSTRAINT_COMMITHOOK
public static let SQLITE_CONSTRAINT_FOREIGNKEY = ResultCode.SQLITE_CONSTRAINT_FOREIGNKEY
public static let SQLITE_CONSTRAINT_FUNCTION = ResultCode.SQLITE_CONSTRAINT_FUNCTION
public static let SQLITE_CONSTRAINT_NOTNULL = ResultCode.SQLITE_CONSTRAINT_NOTNULL
public static let SQLITE_CONSTRAINT_PRIMARYKEY = ResultCode.SQLITE_CONSTRAINT_PRIMARYKEY
public static let SQLITE_CONSTRAINT_TRIGGER = ResultCode.SQLITE_CONSTRAINT_TRIGGER
public static let SQLITE_CONSTRAINT_UNIQUE = ResultCode.SQLITE_CONSTRAINT_UNIQUE
public static let SQLITE_CONSTRAINT_VTAB = ResultCode.SQLITE_CONSTRAINT_VTAB
public static let SQLITE_CONSTRAINT_ROWID = ResultCode.SQLITE_CONSTRAINT_ROWID
public static let SQLITE_CONSTRAINT_PINNED = ResultCode.SQLITE_CONSTRAINT_PINNED
public static let SQLITE_CONSTRAINT_DATATYPE = ResultCode.SQLITE_CONSTRAINT_DATATYPE
public static let SQLITE_NOTICE_RECOVER_WAL = ResultCode.SQLITE_NOTICE_RECOVER_WAL
public static let SQLITE_NOTICE_RECOVER_ROLLBACK = ResultCode.SQLITE_NOTICE_RECOVER_ROLLBACK
public static let SQLITE_WARNING_AUTOINDEX = ResultCode.SQLITE_WARNING_AUTOINDEX
public static let SQLITE_AUTH_USER = ResultCode.SQLITE_AUTH_USER
public static let SQLITE_OK_LOAD_PERMANENTLY = ResultCode.SQLITE_OK_LOAD_PERMANENTLY
public static let SQLITE_OK_SYMLINK = ResultCode.SQLITE_OK_SYMLINK
}
extension DatabaseError {
/// A boolean value indicating if the error has code
/// `SQLITE_ABORT` or `SQLITE_INTERRUPT`.
///
/// Such an error can be thrown when a database has been interrupted, or
/// when the database is suspended.
///
/// See ``DatabaseReader/interrupt()`` and ``Database/suspendNotification``
/// for more information.
public var isInterruptionError: Bool {
switch resultCode {
case .SQLITE_ABORT, .SQLITE_INTERRUPT:
return true
default:
return false
}
}
}
extension DatabaseError: CustomStringConvertible {
/// The error description.
///
/// For example:
///
/// SQLite error 19: NOT NULL constraint failed: player.score
/// - while executing `UPDATE player SET score = ? WHERE email = ?
///
/// The format of the error description may change between GRDB releases,
/// without notice: don't have your application rely on any specific format.
public var description: String {
var description = "SQLite error \(resultCode.rawValue)"
if let message {
description += ": \(message)"
}
if let sql {
description += " - while executing `\(sql.trimmedSQLStatement)`"
}
if publicStatementArguments, let arguments, !arguments.isEmpty {
description += " with arguments \(arguments)"
}
return description
}
/// The error description, where bound parameters, if present, are visible.
///
/// For example:
///
/// SQLite error 19: NOT NULL constraint failed: player.score
/// - while executing `UPDATE player SET score = ? WHERE email = ?
/// with arguments [nil, "arthur@example.com"]
///
/// The format of the error description may change between GRDB releases,
/// without notice: don't have your application rely on any specific format.
///
/// - warning: It is your responsibility to prevent sensitive
/// information from leaking in unexpected locations, so use this
/// property with care.
public var expandedDescription: String {
var description = "SQLite error \(resultCode.rawValue)"
if let message {
description += ": \(message)"
}
if let sql {
description += " - while executing `\(sql.trimmedSQLStatement)`"
}
if let arguments, !arguments.isEmpty {
description += " with arguments \(arguments)"
}
return description
}
}
extension DatabaseError: CustomNSError {
/// Part of the `CustomNSError` conformance.
///
/// Returns `GRDB.DatabaseError`.
public static var errorDomain: String { "GRDB.DatabaseError" }
/// Part of the `CustomNSError` conformance.
///
/// Returns the ``extendedResultCode``.
public var errorCode: Int { Int(extendedResultCode.rawValue) }
/// Part of the `CustomNSError` conformance.
public var errorUserInfo: [String: Any] {
var userInfo = [NSLocalizedDescriptionKey: description]
if let message {
userInfo[NSLocalizedFailureReasonErrorKey] = message
}
return userInfo
}
}
@@ -0,0 +1,482 @@
/// A custom SQL function or aggregate.
///
/// ## Topics
///
/// ### Creating a Custom SQL Function or Aggregate
///
/// - ``init(_:argumentCount:pure:function:)``
/// - ``init(_:argumentCount:pure:aggregate:)``
/// - ``DatabaseAggregate``
///
/// ### Calling an SQL Function or Aggregate
///
/// - ``callAsFunction(_:)``
///
/// ### Built-in Functions
///
/// - ``capitalize``
/// - ``localizedCapitalize``
/// - ``localizedLowercase``
/// - ``localizedUppercase``
/// - ``lowercase``
/// - ``uppercase``
public final class DatabaseFunction: Hashable {
// SQLite identifies functions by (name + argument count)
private struct Identity: Hashable {
let name: String
let nArg: CInt // -1 for variadic functions
}
/// The name of the SQL function
public var name: String { identity.name }
private let identity: Identity
let isPure: Bool
private let kind: Kind
private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) }
/// Creates an SQL function.
///
/// For example:
///
/// ```swift
/// let succ = DatabaseFunction("succ", argumentCount: 1) { dbValues in
/// guard let int = Int.fromDatabaseValue(dbValues[0]) else {
/// return nil
/// }
/// return int + 1
/// }
/// let dbQueue = try DatabaseQueue()
/// try dbQueue.read { db in
/// db.add(function: succ)
/// try Int.fetchOne(db, sql: "SELECT succ(1)")! // 2
/// }
/// ```
///
/// ### Related APIs
///
/// - ``Database/add(function:)``
///
/// - parameters:
/// - name: The function name.
/// - argumentCount: The number of arguments of the function. If
/// omitted, or nil, the function accepts any number of arguments.
/// - pure: Whether the function is "pure", which means that its results
/// only depends on its inputs. When a function is pure, SQLite has
/// the opportunity to perform additional optimizations. Default value
/// is false.
/// - function: A function that takes an array of ``DatabaseValue``
/// arguments, and returns an optional ``DatabaseValueConvertible``
/// such as `Int`, `String`, `Date`, etc. The array is guaranteed to
/// have exactly `argumentCount` elements, provided `argumentCount` is
/// not nil.
public init(
_ name: String,
argumentCount: Int? = nil,
pure: Bool = false,
function: @escaping ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?)
{
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
self.isPure = pure
self.kind = .function{ (argc, argv) in
let arguments = (0..<Int(argc)).map { index in
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
}
return try function(arguments)
}
}
/// Creates an SQL aggregate function.
///
/// For example:
///
/// ```swift
/// struct MySum: DatabaseAggregate {
/// var sum: Int = 0
///
/// mutating func step(_ dbValues: [DatabaseValue]) {
/// if let int = Int.fromDatabaseValue(dbValues[0]) {
/// sum += int
/// }
/// }
///
/// func finalize() -> (any DatabaseValueConvertible)? {
/// return sum
/// }
/// }
///
/// let dbQueue = try DatabaseQueue()
/// let mySum = DatabaseFunction("mySum", argumentCount: 1, aggregate: MySum.self)
/// try dbQueue.write { db in
/// db.add(function: mySum)
/// try db.execute(sql: "CREATE TABLE test(i)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
/// try Int.fetchOne(db, sql: "SELECT mySum(i) FROM test")! // 3
/// }
/// ```
///
/// ### Related APIs
///
/// - ``Database/add(function:)``
///
/// - parameters:
/// - name: The function name.
/// - argumentCount: The number of arguments of the aggregate. If
/// omitted, or nil, the aggregate accepts any number of arguments.
/// - pure: Whether the aggregate is "pure", which means that its
/// results only depends on its inputs. When an aggregate is pure,
/// SQLite has the opportunity to perform additional optimizations.
/// Default value is false.
/// - aggregate: A type that implements the ``DatabaseAggregate``
/// protocol. For each step of the aggregation, its
/// ``DatabaseAggregate/step(_:)`` method is called with an array of
/// ``DatabaseValue`` arguments. The array is guaranteed to have
/// exactly `argumentCount` elements, provided `argumentCount` is
/// not nil.
public init<Aggregate: DatabaseAggregate>(
_ name: String,
argumentCount: Int? = nil,
pure: Bool = false,
aggregate: Aggregate.Type)
{
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
self.isPure = pure
self.kind = .aggregate { Aggregate() }
}
// TODO: GRDB7 -> expose ORDER BY and FILTER when we have distinct types for simple functions and aggregates.
/// Returns an SQL expression that applies the function.
///
/// You can use a `DatabaseFunction` as a regular Swift function. It returns
/// an SQL expression that you can use in the query interface.
///
/// In the example below, `square(Column("score"))` generates the
/// `square(score)` SQL expression:
///
/// ```swift
/// let square = DatabaseFunction("square", argumentCount: 1) { dbValues in
/// guard let int = Int.fromDatabaseValue(dbValues[0]) else {
/// return nil
/// }
/// return int * int
/// }
/// let dbQueue = try DatabaseQueue()
/// try dbQueue.read { db in
/// db.add(function: square)
///
/// // SELECT square(score) FROM player
/// let squaredScores = let Player
/// .select(square(Column("score")), as: Int.self)
/// .fetchAll(db)
/// }
/// ```
public func callAsFunction(_ arguments: any SQLExpressible...) -> SQLExpression {
switch kind {
case .function:
return .simpleFunction(
name,
arguments.map(\.sqlExpression),
isPure: isPure,
isJSONValue: false)
case .aggregate:
return .aggregateFunction(
name,
arguments.map(\.sqlExpression),
isDistinct: false,
ordering: nil,
filter: nil,
isJSONValue: false)
}
}
/// Calls sqlite3_create_function_v2
/// See <https://sqlite.org/c3ref/create_function.html>
func install(in db: Database) {
// Retain the function definition
let definition = kind.definition
let definitionP = Unmanaged.passRetained(definition).toOpaque()
let code = sqlite3_create_function_v2(
db.sqliteConnection,
identity.name,
identity.nArg,
eTextRep,
definitionP,
kind.xFunc,
kind.xStep,
kind.xFinal,
{ definitionP in
// Release the function definition
Unmanaged<AnyObject>.fromOpaque(definitionP!).release()
})
guard code == SQLITE_OK else {
// Assume a GRDB bug: there is no point throwing any error.
fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage))
}
}
/// Calls sqlite3_create_function_v2
/// See <https://sqlite.org/c3ref/create_function.html>
func uninstall(in db: Database) {
let code = sqlite3_create_function_v2(
db.sqliteConnection,
identity.name,
identity.nArg,
eTextRep,
nil, nil, nil, nil, nil)
guard code == SQLITE_OK else {
// Assume a GRDB bug: there is no point throwing any error.
fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage))
}
}
/// The way to compute the result of a function.
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
private class FunctionDefinition {
let compute: (CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?
init(compute: @escaping (CInt, UnsafeMutablePointer<OpaquePointer?>?)
throws -> (any DatabaseValueConvertible)?)
{
self.compute = compute
}
}
/// The way to start an aggregate.
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
private class AggregateDefinition {
let makeAggregate: () -> any DatabaseAggregate
init(makeAggregate: @escaping () -> any DatabaseAggregate) {
self.makeAggregate = makeAggregate
}
}
/// The current state of an aggregate, storable in SQLite
private class AggregateContext {
var aggregate: any DatabaseAggregate
var hasErrored = false
init(aggregate: some DatabaseAggregate) {
self.aggregate = aggregate
}
}
/// A function kind: an "SQL function" or an "aggregate".
/// See <http://sqlite.org/capi3ref.html#sqlite3_create_function>
private enum Kind {
/// A regular function: SELECT f(1)
case function((CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?)
/// An aggregate: SELECT f(foo) FROM bar GROUP BY baz
case aggregate(() -> any DatabaseAggregate)
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
var definition: AnyObject {
switch self {
case .function(let compute):
return FunctionDefinition(compute: compute)
case .aggregate(let makeAggregate):
return AggregateDefinition(makeAggregate: makeAggregate)
}
}
/// Feeds the `xFunc` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
var xFunc: (@convention(c) (OpaquePointer?, CInt, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? {
guard case .function = self else { return nil }
return { (sqliteContext, argc, argv) in
let definition = Unmanaged<FunctionDefinition>
.fromOpaque(sqlite3_user_data(sqliteContext))
.takeUnretainedValue()
do {
try DatabaseFunction.report(
result: definition.compute(argc, argv),
in: sqliteContext)
} catch {
DatabaseFunction.report(error: error, in: sqliteContext)
}
}
}
/// Feeds the `xStep` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
var xStep: (@convention(c) (OpaquePointer?, CInt, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? {
guard case .aggregate = self else { return nil }
return { (sqliteContext, argc, argv) in
let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext)
let aggregateContext = aggregateContextU.takeUnretainedValue()
assert(!aggregateContext.hasErrored) // assert SQLite behavior
do {
let arguments = (0..<Int(argc)).map { index in
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
}
try aggregateContext.aggregate.step(arguments)
} catch {
aggregateContext.hasErrored = true
DatabaseFunction.report(error: error, in: sqliteContext)
}
}
}
/// Feeds the `xFinal` parameter of sqlite3_create_function_v2
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
var xFinal: (@convention(c) (OpaquePointer?) -> Void)? {
guard case .aggregate = self else { return nil }
return { (sqliteContext) in
let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext)
let aggregateContext = aggregateContextU.takeUnretainedValue()
aggregateContextU.release()
guard !aggregateContext.hasErrored else {
return
}
do {
try DatabaseFunction.report(
result: aggregateContext.aggregate.finalize(),
in: sqliteContext)
} catch {
DatabaseFunction.report(error: error, in: sqliteContext)
}
}
}
}
/// Helper function that extracts the current state of an aggregate from an
/// sqlite function execution context.
///
/// The result must be released when the aggregate concludes.
///
/// See <https://sqlite.org/c3ref/context.html>
/// See <https://sqlite.org/c3ref/aggregate_context.html>
private static func unmanagedAggregateContext(_ sqliteContext: OpaquePointer?) -> Unmanaged<AggregateContext> {
// > The first time the sqlite3_aggregate_context(C,N) routine is called
// > for a particular aggregate function, SQLite allocates N of memory,
// > zeroes out that memory, and returns a pointer to the new memory.
// > On second and subsequent calls to sqlite3_aggregate_context() for
// > the same aggregate function instance, the same buffer is returned.
let stride = MemoryLayout<Unmanaged<AggregateContext>>.stride
let aggregateContextBufferP = UnsafeMutableRawBufferPointer(
start: sqlite3_aggregate_context(sqliteContext, CInt(stride))!,
count: stride)
if aggregateContextBufferP.contains(where: { $0 != 0 }) {
// Buffer contains non-zero byte: load aggregate context
let aggregateContextP = aggregateContextBufferP
.baseAddress!
.assumingMemoryBound(to: Unmanaged<AggregateContext>.self)
return aggregateContextP.pointee
} else {
// Buffer contains null pointer: create aggregate context.
let aggregate = Unmanaged<AggregateDefinition>.fromOpaque(sqlite3_user_data(sqliteContext))
.takeUnretainedValue()
.makeAggregate()
let aggregateContext = AggregateContext(aggregate: aggregate)
// retain and store in SQLite's buffer
let aggregateContextU = Unmanaged.passRetained(aggregateContext)
let aggregateContextP = aggregateContextU.toOpaque()
withUnsafeBytes(of: aggregateContextP) {
aggregateContextBufferP.copyMemory(from: $0)
}
return aggregateContextU
}
}
private static func report(result: (any DatabaseValueConvertible)?, in sqliteContext: OpaquePointer?) {
switch result?.databaseValue.storage ?? .null {
case .null:
sqlite3_result_null(sqliteContext)
case .int64(let int64):
sqlite3_result_int64(sqliteContext, int64)
case .double(let double):
sqlite3_result_double(sqliteContext, double)
case .string(let string):
sqlite3_result_text(sqliteContext, string, -1, SQLITE_TRANSIENT)
case .blob(let data):
data.withUnsafeBytes {
sqlite3_result_blob(sqliteContext, $0.baseAddress, CInt($0.count), SQLITE_TRANSIENT)
}
}
}
private static func report(error: Error, in sqliteContext: OpaquePointer?) {
if let error = error as? DatabaseError {
if let message = error.message {
sqlite3_result_error(sqliteContext, message, -1)
}
sqlite3_result_error_code(sqliteContext, error.extendedResultCode.rawValue)
} else {
sqlite3_result_error(sqliteContext, "\(error)", -1)
}
}
}
extension DatabaseFunction {
public func hash(into hasher: inout Hasher) {
hasher.combine(identity)
}
/// Two functions are equal if they share the same name and arity.
public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool {
lhs.identity == rhs.identity
}
}
/// The protocol for custom SQLite aggregates.
///
/// For example:
///
/// ```swift
/// struct MySum : DatabaseAggregate {
/// var sum: Int = 0
///
/// mutating func step(_ dbValues: [DatabaseValue]) {
/// if let int = Int.fromDatabaseValue(dbValues[0]) {
/// sum += int
/// }
/// }
///
/// func finalize() -> (any DatabaseValueConvertible)? {
/// return sum
/// }
/// }
///
/// let dbQueue = try DatabaseQueue()
/// let mySum = DatabaseFunction("mySum", argumentCount: 1, aggregate: MySum.self)
/// try dbQueue.write { db in
/// db.add(function: mySum)
/// try db.execute(sql: "CREATE TABLE test(i)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
/// try Int.fetchOne(db, sql: "SELECT mysum(i) FROM test")! // 3
/// }
/// ```
public protocol DatabaseAggregate {
/// Creates an aggregate.
///
/// A new instance is created for each aggregation.
init()
/// Updates the aggregated value for one step of the aggregation.
///
/// This method is called once for each step of the aggregation.
///
/// The `dbValues` argument contains as many values as given to the SQL
/// aggregate function:
///
/// ```sql
/// -- One value
/// SELECT maxLength(name) FROM player
///
/// -- Two values
/// SELECT maxFullNameLength(firstName, lastName) FROM player
/// ```
mutating func step(_ dbValues: [DatabaseValue]) throws
/// Returns the aggregated value.
func finalize() throws -> (any DatabaseValueConvertible)?
}
@@ -0,0 +1,893 @@
import Dispatch
import Foundation
#if os(iOS)
import UIKit
#endif
public final class DatabasePool {
private let writer: SerializedDatabase
/// The pool of reader connections.
/// It is constant, until close() sets it to nil.
private var readerPool: Pool<SerializedDatabase>?
@LockedBox var databaseSnapshotCount = 0
/// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers.
private var suspensionObservers: [NSObjectProtocol] = []
// MARK: - Database Information
public var configuration: Configuration {
writer.configuration
}
/// The path to the database.
public var path: String {
writer.path
}
// MARK: - Initializer
/// Opens or creates an SQLite database.
///
/// For example:
///
/// ```swift
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
/// ```
///
/// The SQLite connections are closed when the database pool
/// gets deallocated.
///
/// - parameters:
/// - path: The path to the database file.
/// - configuration: A configuration.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public init(path: String, configuration: Configuration = Configuration()) throws {
GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1")
// Writer
writer = try SerializedDatabase(
path: path,
configuration: configuration,
defaultLabel: "GRDB.DatabasePool",
purpose: "writer")
// Readers
var readerConfiguration = DatabasePool.readerConfiguration(configuration)
// Readers can't allow dangling transactions because there's no
// guarantee that one can get the same reader later in order to close
// an opened transaction.
readerConfiguration.allowsUnsafeTransactions = false
var readerCount = 0
readerPool = Pool(
maximumCount: configuration.maximumReaderCount,
qos: configuration.readQoS,
makeElement: {
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
return try SerializedDatabase(
path: path,
configuration: readerConfiguration,
defaultLabel: "GRDB.DatabasePool",
purpose: "reader.\(readerCount)")
})
// Set up journal mode unless readonly
if !configuration.readonly {
switch configuration.journalMode {
case .default, .wal:
try writer.sync {
try $0.setUpWALMode()
}
}
}
setupSuspension()
// Be a nice iOS citizen, and don't consume too much memory
// See https://github.com/groue/GRDB.swift/#memory-management
#if os(iOS)
if configuration.automaticMemoryManagement {
setupMemoryManagement()
}
#endif
}
deinit {
// Remove block-based Notification observers.
suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:))
// Undo job done in setupMemoryManagement()
//
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error
// Explicit unregistration is required before macOS 10.11.
NotificationCenter.default.removeObserver(self)
// Close reader connections before the writer connection.
// Context: https://github.com/groue/GRDB.swift/issues/739
readerPool = nil
}
/// Returns a Configuration suitable for readonly connections on a
/// WAL database.
private static func readerConfiguration(_ configuration: Configuration) -> Configuration {
var configuration = configuration
configuration.readonly = true
// Readers use deferred transactions by default.
// Other transaction kinds are forbidden by SQLite in read-only connections.
configuration.defaultTransactionKind = .deferred
// <https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode>
// > But there are some obscure cases where a query against a WAL-mode
// > database can return SQLITE_BUSY, so applications should be prepared
// > for that happenstance.
// >
// > - If another database connection has the database mode open in
// > exclusive locking mode [...]
// > - When the last connection to a particular database is closing,
// > that connection will acquire an exclusive lock for a short time
// > while it cleans up the WAL and shared-memory files [...]
// > - If the last connection to a database crashed, then the first new
// > connection to open the database will start a recovery process. An
// > exclusive lock is held during recovery. [...]
//
// The whole point of WAL readers is to avoid SQLITE_BUSY, so let's
// setup a busy handler for pool readers, in order to workaround those
// "obscure cases" that may happen when the database is shared between
// multiple processes.
if configuration.readonlyBusyMode == nil {
configuration.readonlyBusyMode = .timeout(10)
}
return configuration
}
}
// @unchecked because of databaseSnapshotCount, readerPool and suspensionObservers
extension DatabasePool: @unchecked Sendable { }
extension DatabasePool {
// MARK: - Memory management
/// Frees as much memory as possible, by disposing non-essential memory.
///
/// This method is synchronous, and blocks the current thread until all
/// database accesses are completed.
///
/// This method closes all read-only connections, unless the
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
/// is set.
///
/// - warning: This method can prevent concurrent reads from executing,
/// until it returns. Prefer ``releaseMemoryEventually()`` if you intend
/// to keep on using the database while releasing memory.
public func releaseMemory() {
// Release writer memory
writer.sync { $0.releaseMemory() }
if configuration.persistentReadOnlyConnections {
// Keep existing readers
readerPool?.forEach { reader in
reader.sync { $0.releaseMemory() }
}
} else {
// Release readers memory by closing all connections.
//
// We must use a barrier in order to guarantee that memory has been
// freed (reader connections closed) when the method exits, as
// documented.
//
// Without the barrier, connections would only close _eventually_ (after
// their eventual concurrent jobs have completed).
readerPool?.barrier {
readerPool?.removeAll()
}
}
}
/// Eventually frees as much memory as possible, by disposing
/// non-essential memory.
///
/// This method eventually closes all read-only connections, unless the
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
/// is set.
///
/// Unlike ``releaseMemory()``, this method does not prevent concurrent
/// database accesses when it is executing. But it does not notify when
/// non-essential memory has been freed.
public func releaseMemoryEventually() {
if configuration.persistentReadOnlyConnections {
// Keep existing readers
readerPool?.forEach { reader in
reader.async { $0.releaseMemory() }
}
} else {
// Release readers memory by eventually closing all reader connections
// (they will close after their current jobs have completed).
readerPool?.removeAll()
}
// Release writer memory eventually.
writer.async { $0.releaseMemory() }
}
#if os(iOS)
/// Listens to UIApplicationDidEnterBackgroundNotification and
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
/// as much memory as possible.
private func setupMemoryManagement() {
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
center.addObserver(
self,
selector: #selector(DatabasePool.applicationDidEnterBackground(_:)),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
}
@objc
private func applicationDidEnterBackground(_ notification: NSNotification) {
guard let application = notification.object as? UIApplication else {
return
}
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
if task == .invalid {
// Release memory synchronously
releaseMemory()
} else {
// Release memory eventually.
//
// We don't know when reader connections will be closed (because
// they may be currently in use), so we don't quite know when
// reader memory will be freed (which would be the ideal timing for
// ending our background task).
//
// So let's just end the background task after the writer connection
// has freed its memory. That's better than nothing.
releaseMemoryEventually()
writer.async { _ in
application.endBackgroundTask(task)
}
}
}
@objc
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
releaseMemoryEventually()
}
#endif
}
extension DatabasePool: DatabaseReader {
public func close() throws {
try readerPool?.barrier {
// Close writer connection first. If we can't close it,
// don't close readers.
//
// This allows us to exit this method as fully closed (read and
// writes fail), or not closed at all (reads and writes succeed).
//
// Unfortunately, this introduces a regression for
// https://github.com/groue/GRDB.swift/issues/739.
// TODO: fix this regression.
try writer.sync { try $0.close() }
// OK writer is closed. Now close readers and
// eventually prevent any future read access
defer { readerPool = nil }
try readerPool?.forEach { reader in
try reader.sync { try $0.close() }
}
}
}
// MARK: - Interrupting Database Operations
public func interrupt() {
writer.interrupt()
readerPool?.forEach { $0.interrupt() }
}
// MARK: - Database Suspension
func suspend() {
if configuration.readonly {
// read-only WAL connections can't acquire locks and do not need to
// be suspended.
return
}
writer.suspend()
}
func resume() {
if configuration.readonly {
// read-only WAL connections can't acquire locks and do not need to
// be suspended.
return
}
writer.resume()
}
private func setupSuspension() {
if configuration.observesSuspensionNotifications {
let center = NotificationCenter.default
suspensionObservers.append(center.addObserver(
forName: Database.suspendNotification,
object: nil,
queue: nil,
using: { [weak self] _ in self?.suspend() }
))
suspensionObservers.append(center.addObserver(
forName: Database.resumeNotification,
object: nil,
queue: nil,
using: { [weak self] _ in self?.resume() }
))
}
}
// MARK: - Reading from Database
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func read<T>(_ value: (Database) throws -> T) throws -> T {
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
return try readerPool.get { reader in
try reader.sync { db in
try db.isolated {
try db.clearSchemaCacheIfNeeded()
return try value(db)
}
}
}
}
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
}
readerPool.asyncGet { result in
do {
let (reader, releaseReader) = try result.get()
// Second async jump because that's how `Pool.async` has to be used.
reader.async { db in
defer {
try? db.commit() // Ignore commit error
releaseReader(.reuse)
}
do {
// The block isolation comes from the DEFERRED transaction.
try db.beginTransaction(.deferred)
try db.clearSchemaCacheIfNeeded()
value(.success(db))
} catch {
value(.failure(error))
}
}
} catch {
value(.failure(error))
}
}
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
return try readerPool.get { reader in
try reader.sync { db in
try db.clearSchemaCacheIfNeeded()
return try value(db)
}
}
}
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
}
readerPool.asyncGet { result in
do {
let (reader, releaseReader) = try result.get()
// Second async jump because that's how `Pool.async` has to be used.
reader.async { db in
defer {
releaseReader(.reuse)
}
do {
try db.clearSchemaCacheIfNeeded()
value(.success(db))
} catch {
value(.failure(error))
}
}
} catch {
value(.failure(error))
}
}
}
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
if let reader = currentReader {
return try reader.reentrantSync(value)
} else if writer.onValidQueue {
return try writer.execute(value)
} else {
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
return try readerPool.get { reader in
try reader.sync { db in
try db.clearSchemaCacheIfNeeded()
return try value(db)
}
}
}
}
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
// The semaphore that blocks until futureResult is defined:
let futureSemaphore = DispatchSemaphore(value: 0)
var futureResult: Result<T, Error>? = nil
asyncConcurrentRead { dbResult in
// Fetch and release the future
futureResult = dbResult.flatMap { db in Result { try value(db) } }
futureSemaphore.signal()
}
return DatabaseFuture {
// Block the future until results are fetched
_ = futureSemaphore.wait(timeout: .distantFuture)
return try futureResult!.get()
}
}
public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
asyncConcurrentRead(value)
}
/// Performs an asynchronous read access.
///
/// This method must be called from the writer dispatch queue, outside of
/// any transaction. You'll get a fatal error otherwise.
///
/// The `value` function is guaranteed to see the database in the last
/// committed state at the moment this method is called. Eventual
/// concurrent database updates are not visible from the function.
///
/// This method returns as soon as the isolation guarantee described above
/// has been established.
///
/// In the example below, the number of players is fetched concurrently with
/// the player insertion. Yet it is guaranteed to be zero:
///
/// ```swift
/// try writer.asyncWriteWithoutTransaction { db in
/// // Delete all players
/// try Player.deleteAll()
///
/// // Count players concurrently
/// writer.asyncConcurrentRead { dbResult in
/// do {
/// let db = try dbResult.get()
/// // Guaranteed to be zero
/// let count = try Player.fetchCount(db)
/// } catch {
/// // Handle error
/// }
/// }
///
/// // Insert a player
/// try Player(...).insert(db)
/// }
/// ```
///
/// - parameter value: A function that accesses the database.
public func asyncConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
GRDBPrecondition(!db.isInsideTransaction, """
must not be called from inside a transaction. \
If this error is raised from a DatabasePool.write block, use \
DatabasePool.writeWithoutTransaction instead (and use \
transactions when needed).
""")
}
// The semaphore that blocks the writing dispatch queue until snapshot
// isolation has been established:
let isolationSemaphore = DispatchSemaphore(value: 0)
do {
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
let (reader, releaseReader) = try readerPool.get()
reader.async { db in
defer {
try? db.commit() // Ignore commit error
releaseReader(.reuse)
}
do {
// https://www.sqlite.org/isolation.html
//
// > In WAL mode, SQLite exhibits "snapshot isolation". When
// > a read transaction starts, that reader continues to see
// > an unchanging "snapshot" of the database file as it
// > existed at the moment in time when the read transaction
// > started. Any write transactions that commit while the
// > read transaction is active are still invisible to the
// > read transaction, because the reader is seeing a
// > snapshot of database file from a prior moment in time.
//
// That's exactly what we need. But what does "when read
// transaction starts" mean?
//
// http://www.sqlite.org/lang_transaction.html
//
// > Deferred [transaction] means that no locks are acquired
// > on the database until the database is first accessed.
// > [...] Locks are not acquired until the first read or
// > write operation. [...] Because the acquisition of locks
// > is deferred until they are needed, it is possible that
// > another thread or process could create a separate
// > transaction and write to the database after the BEGIN
// > on the current thread has executed.
//
// Now that's precise enough: SQLite defers snapshot
// isolation until the first SELECT:
//
// Reader Writer
// BEGIN DEFERRED TRANSACTION
// UPDATE ... (1)
// Here the change (1) is visible from the reader
// SELECT ...
// UPDATE ... (2)
// Here the change (2) is not visible from the reader
//
// We thus have to perform a select that establishes the
// snapshot isolation before we release the writer queue:
//
// Reader Writer
// BEGIN DEFERRED TRANSACTION
// SELECT anything
// UPDATE ... (1)
// Here the change (1) is not visible from the reader
//
// Since any select goes, use `PRAGMA schema_version`.
try db.beginTransaction(.deferred)
try db.clearSchemaCacheIfNeeded()
} catch {
isolationSemaphore.signal()
value(.failure(error))
return
}
// Now that we have an isolated snapshot of the last commit, we
// can release the writer queue.
isolationSemaphore.signal()
value(.success(db))
}
} catch {
isolationSemaphore.signal()
value(.failure(error))
}
// Block the writer queue until snapshot isolation success or error
_ = isolationSemaphore.wait(timeout: .distantFuture)
}
/// Invalidates open read-only SQLite connections.
///
/// After this method is called, read-only database access methods will use
/// new SQLite connections.
///
/// Eventual concurrent read-only accesses are not interrupted, and
/// proceed until completion.
///
/// - This method closes all read-only connections, even if the
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
/// is set.
public func invalidateReadOnlyConnections() {
readerPool?.removeAll()
}
/// Returns a reader that can be used from the current dispatch queue,
/// if any.
private var currentReader: SerializedDatabase? {
guard let readerPool else {
return nil
}
var readers: [SerializedDatabase] = []
readerPool.forEach { reader in
// We can't check for reader.onValidQueue here because
// Pool.forEach() runs its closure argument in some arbitrary
// dispatch queue. We thus extract the reader so that we can query
// it below.
readers.append(reader)
}
// Now the readers array contains some readers. The pool readers may
// already be different, because some other thread may have started
// a new read, for example.
//
// This doesn't matter: the reader we are looking for is already on
// its own dispatch queue. If it exists, is still in use, thus still
// in the pool, and thus still relevant for our check:
return readers.first { $0.onValidQueue }
}
// MARK: - WAL Snapshot Transactions
// swiftlint:disable:next line_length
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
/// Returns a long-lived WAL snapshot transaction on a reader connection.
func walSnapshotTransaction() throws -> WALSnapshotTransaction {
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
let (reader, releaseReader) = try readerPool.get()
return try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
// Discard the connection if the transaction could not be
// properly ended. If we'd reuse it, the next read would
// fail because we'd fail starting a read transaction.
releaseReader(isInsideTransaction ? .discard : .reuse)
})
}
/// Returns a long-lived WAL snapshot transaction on a reader connection.
///
/// - important: The `completion` argument is executed in a serial
/// dispatch queue, so make sure you use the transaction asynchronously.
func asyncWALSnapshotTransaction(_ completion: @escaping (Result<WALSnapshotTransaction, Error>) -> Void) {
guard let readerPool else {
completion(.failure(DatabaseError.connectionIsClosed()))
return
}
readerPool.asyncGet { result in
completion(result.flatMap { reader, releaseReader in
Result {
try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
// Discard the connection if the transaction could not be
// properly ended. If we'd reuse it, the next read would
// fail because we'd fail starting a read transaction.
releaseReader(isInsideTransaction ? .discard : .reuse)
})
}
})
}
}
#endif
// MARK: - Database Observation
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
if configuration.readonly {
// The easy case: the database does not change
return _addReadOnly(
observation: observation,
scheduling: scheduler,
onChange: onChange)
} else if observation.requiresWriteAccess {
// Observe from the writer database connection.
return _addWriteOnly(
observation: observation,
scheduling: scheduler,
onChange: onChange)
} else {
// DatabasePool can perform concurrent observation
return _addConcurrent(
observation: observation,
scheduling: scheduler,
onChange: onChange)
}
}
/// A concurrent observation fetches the initial value without waiting for
/// the writer.
private func _addConcurrent<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
assert(!configuration.readonly, "Use _addReadOnly(observation:) instead")
assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead")
let observer = ValueConcurrentObserver(
dbPool: self,
scheduler: scheduler,
trackingMode: observation.trackingMode,
reducer: observation.makeReducer(),
events: observation.events,
onChange: onChange)
return observer.start()
}
}
extension DatabasePool: DatabaseWriter {
// MARK: - Writing in Database
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func writeWithoutTransaction<T>(_ updates: (Database) throws -> T) rethrows -> T {
try writer.sync(updates)
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func barrierWriteWithoutTransaction<T>(_ updates: (Database) throws -> T) throws -> T {
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
return try readerPool.barrier {
try writer.sync(updates)
}
}
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
guard let readerPool else {
updates(.failure(DatabaseError.connectionIsClosed()))
return
}
readerPool.asyncBarrier {
self.writer.sync { updates(.success($0)) }
}
}
/// Wraps database operations inside a database transaction.
///
/// The `updates` function runs in the writer dispatch queue, serialized
/// with all database updates.
///
/// If `updates` throws an error, the transaction is rollbacked and the
/// error is rethrown. If it returns
/// ``Database/TransactionCompletion/rollback``, the transaction is also
/// rollbacked, but no error is thrown.
///
/// For example:
///
/// ```swift
/// try dbPool.writeInTransaction { db in
/// try Player(name: "Arthur").insert(db)
/// try Player(name: "Barbara").insert(db)
/// return .commit
/// }
/// ```
///
/// - precondition: This method is not reentrant.
/// - parameters:
/// - kind: The transaction type (default nil). If nil, the transaction
/// type is the ``Configuration/defaultTransactionKind`` of the
/// the ``configuration``.
/// - updates: A function that updates the database.
/// - throws: The error thrown by `updates`, or by the wrapping transaction.
public func writeInTransaction(
_ kind: Database.TransactionKind? = nil,
_ updates: (Database) throws -> Database.TransactionCompletion)
throws
{
try writer.sync { db in
try db.inTransaction(kind) {
try updates(db)
}
}
}
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
try writer.reentrantSync(updates)
}
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
writer.async(updates)
}
}
extension DatabasePool {
// MARK: - Snapshots
/// Creates a database snapshot that serializes accesses to an unchanging
/// database content, as it exists at the moment the snapshot is created.
///
/// It is a programmer error to create a snapshot from the writer dispatch
/// queue when a transaction is opened:
///
/// ```swift
/// try dbPool.write { db in
/// try Player.deleteAll()
///
/// // fatal error: makeSnapshot() must not be called from inside a transaction
/// let snapshot = try dbPool.makeSnapshot()
/// }
/// ```
///
/// To avoid this fatal error, create the snapshot *before* or *after*
/// the transaction:
///
/// ```swift
/// let snapshot = try dbPool.makeSnapshot() // OK
///
/// try dbPool.writeWithoutTransaction { db in
/// let snapshot = try dbPool.makeSnapshot() // OK
///
/// try db.inTransaction {
/// try Player.deleteAll()
/// return .commit
/// }
///
/// // OK
/// let snapshot = try dbPool.makeSnapshot() // OK
/// }
///
/// let snapshot = try dbPool.makeSnapshot() // OK
/// ```
public func makeSnapshot() throws -> DatabaseSnapshot {
// Sanity check
if writer.onValidQueue {
writer.execute { db in
GRDBPrecondition(
!db.isInsideTransaction,
"makeSnapshot() must not be called from inside a transaction.")
}
}
return try DatabaseSnapshot(
path: path,
configuration: DatabasePool.readerConfiguration(writer.configuration),
defaultLabel: "GRDB.DatabasePool",
purpose: "snapshot.\($databaseSnapshotCount.increment())")
}
// swiftlint:disable:next line_length
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
/// Creates a database snapshot that allows concurrent accesses to an
/// unchanging database content, as it exists at the moment the snapshot
/// is created.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
/// or if this method is called from a write transaction, or if the
/// wal file is missing or truncated (size zero).
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
public func makeSnapshotPool() throws -> DatabaseSnapshotPool {
try unsafeReentrantRead { db in
try DatabaseSnapshotPool(db)
}
}
#endif
}
@@ -0,0 +1,5 @@
#if canImport(Combine)
/// A namespace for database Combine publishers.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public enum DatabasePublishers { }
#endif
@@ -0,0 +1,524 @@
import Foundation
#if os(iOS)
import UIKit
#endif
public final class DatabaseQueue {
private let writer: SerializedDatabase
/// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers.
private var suspensionObservers: [NSObjectProtocol] = []
// MARK: - Configuration
public var configuration: Configuration {
writer.configuration
}
public var path: String {
writer.path
}
// MARK: - Initializers
/// Opens or creates an SQLite database.
///
/// For example:
///
/// ```swift
/// let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
/// ```
///
/// The SQLite connection is closed when the database queue
/// gets deallocated.
///
/// - parameters:
/// - path: The path to the database file.
/// - configuration: A configuration.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public init(path: String, configuration: Configuration = Configuration()) throws {
// DatabaseQueue can't perform parallel reads
var configuration = configuration
configuration.maximumReaderCount = 1
writer = try SerializedDatabase(
path: path,
configuration: configuration,
defaultLabel: "GRDB.DatabaseQueue")
// Set up journal mode unless readonly
if !configuration.readonly {
switch configuration.journalMode {
case .default:
break
case .wal:
try writer.sync {
try $0.setUpWALMode()
}
}
}
setupSuspension()
// Be a nice iOS citizen, and don't consume too much memory
// See https://github.com/groue/GRDB.swift/#memory-management
#if os(iOS)
if configuration.automaticMemoryManagement {
setupMemoryManagement()
}
#endif
}
/// Opens an in-memory SQLite database.
///
/// To create an independent in-memory database, don't pass any name. The
/// database memory is released when the database queue is deallocated:
///
/// ```swift
/// // An independent in-memory database
/// let dbQueue = try DatabaseQueue()
/// ```
///
/// When you need to open several connections to the same in-memory
/// database, give it a name:
///
/// ```swift
/// // A shared in-memory database
/// let dbQueue = try DatabaseQueue(named: "myDatabase")
/// ```
///
/// In this case, the database is automatically deleted and memory is
/// reclaimed when the last connection to the database of the given
/// name closes.
///
/// Related SQLite documentation: <https://www.sqlite.org/inmemorydb.html>
///
/// - parameter name: When nil, an independent in-memory database opens.
/// Otherwise, the shared in-memory database of the given name opens.
/// - parameter configuration: A configuration.
public init(named name: String? = nil, configuration: Configuration = Configuration()) throws {
let path: String
if let name {
path = "file:\(name)?mode=memory&cache=shared"
} else {
path = ":memory:"
}
writer = try SerializedDatabase(
path: path,
configuration: configuration,
defaultLabel: "GRDB.DatabaseQueue")
}
deinit {
// Remove block-based Notification observers.
suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:))
// Undo job done in setupMemoryManagement()
//
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error
// Explicit unregistration is required before macOS 10.11.
NotificationCenter.default.removeObserver(self)
}
}
// @unchecked because of suspensionObservers
extension DatabaseQueue: @unchecked Sendable { }
extension DatabaseQueue {
// MARK: - Memory management
/// Free as much memory as possible.
///
/// This method blocks the current thread until all database accesses are completed.
public func releaseMemory() {
writer.sync { $0.releaseMemory() }
}
#if os(iOS)
/// Listens to UIApplicationDidEnterBackgroundNotification and
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
/// as much memory as possible.
private func setupMemoryManagement() {
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil)
center.addObserver(
self,
selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
}
@objc
private func applicationDidEnterBackground(_ notification: NSNotification) {
guard let application = notification.object as? UIApplication else {
return
}
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
if task == .invalid {
// Release memory synchronously
releaseMemory()
} else {
// Release memory asynchronously
writer.async { db in
db.releaseMemory()
application.endBackgroundTask(task)
}
}
}
@objc
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
writer.async { db in
db.releaseMemory()
}
}
#endif
}
extension DatabaseQueue: DatabaseReader {
public func close() throws {
try writer.sync { try $0.close() }
}
// MARK: - Interrupting Database Operations
public func interrupt() {
writer.interrupt()
}
// MARK: - Database Suspension
func suspend() {
writer.suspend()
}
func resume() {
writer.resume()
}
private func setupSuspension() {
if configuration.observesSuspensionNotifications {
let center = NotificationCenter.default
suspensionObservers.append(center.addObserver(
forName: Database.suspendNotification,
object: nil,
queue: nil,
using: { [weak self] _ in self?.suspend() }
))
suspensionObservers.append(center.addObserver(
forName: Database.resumeNotification,
object: nil,
queue: nil,
using: { [weak self] _ in self?.resume() }
))
}
}
// MARK: - Reading from Database
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func read<T>(_ value: (Database) throws -> T) throws -> T {
try writer.sync { db in
try db.isolated(readOnly: true) {
try value(db)
}
}
}
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
writer.async { db in
defer {
// Ignore error because we can not notify it.
try? db.commit()
try? db.endReadOnly()
}
do {
// Enter read-only mode before starting a transaction, so that the
// transaction commit does not trigger database observation.
// See <https://github.com/groue/GRDB.swift/pull/1213>.
try db.beginReadOnly()
try db.beginTransaction(.deferred)
value(.success(db))
} catch {
value(.failure(error))
}
}
}
public func unsafeRead<T>(_ value: (Database) throws -> T) rethrows -> T {
try writer.sync(value)
}
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
writer.async { value(.success($0)) }
}
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) rethrows -> T {
try writer.reentrantSync(value)
}
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
// DatabaseQueue can't perform parallel reads.
// Perform a blocking read instead.
return DatabaseFuture(Result {
// Check that we're on the writer queue, as documented
try writer.execute { db in
try db.isolated(readOnly: true) {
try value(db)
}
}
})
}
public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
// Check that we're on the writer queue...
writer.execute { db in
// ... and that no transaction is opened.
GRDBPrecondition(!db.isInsideTransaction, "must not be called from inside a transaction.")
defer {
// Ignore error because we can not notify it.
try? db.commit()
try? db.endReadOnly()
}
do {
// Enter read-only mode before starting a transaction, so that the
// transaction commit does not trigger database observation.
// See <https://github.com/groue/GRDB.swift/pull/1213>.
try db.beginReadOnly()
try db.beginTransaction(.deferred)
value(.success(db))
} catch {
value(.failure(error))
}
}
}
// MARK: - Database Observation
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
if configuration.readonly {
// The easy case: the database does not change
return _addReadOnly(
observation: observation,
scheduling: scheduler,
onChange: onChange)
} else {
// Observe from the writer database connection.
return _addWriteOnly(
observation: observation,
scheduling: scheduler,
onChange: onChange)
}
}
}
extension DatabaseQueue: DatabaseWriter {
// MARK: - Writing in Database
/// Wraps database operations inside a database transaction.
///
/// The `updates` function runs in the writer dispatch queue, serialized
/// with all database updates.
///
/// If `updates` throws an error, the transaction is rollbacked and the
/// error is rethrown. If it returns
/// ``Database/TransactionCompletion/rollback``, the transaction is also
/// rollbacked, but no error is thrown.
///
/// For example:
///
/// ```swift
/// try dbQueue.inTransaction { db in
/// try Player(name: "Arthur").insert(db)
/// try Player(name: "Barbara").insert(db)
/// return .commit
/// }
/// ```
///
/// - parameters:
/// - kind: The transaction type (default nil). If nil, the transaction
/// type is the ``Configuration/defaultTransactionKind`` of the
/// the ``configuration``.
/// - updates: A function that updates the database.
/// - throws: The error thrown by `updates`, or by the wrapping transaction.
public func inTransaction(
_ kind: Database.TransactionKind? = nil,
_ updates: (Database) throws -> Database.TransactionCompletion)
throws
{
try writer.sync { db in
try db.inTransaction(kind) {
try updates(db)
}
}
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func writeWithoutTransaction<T>(_ updates: (Database) throws -> T) rethrows -> T {
try writer.sync(updates)
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func barrierWriteWithoutTransaction<T>(_ updates: (Database) throws -> T) throws -> T {
try writer.sync(updates)
}
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
writer.async { updates(.success($0)) }
}
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method is identical to
/// ``DatabaseWriter/writeWithoutTransaction(_:)-4qh1w``
///
/// For example:
///
/// ```swift
/// let newPlayerCount = try dbQueue.inDatabase { db in
/// try Player(name: "Arthur").insert(db)
/// return try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations run in the writer dispatch queue, serialized
/// with all database updates performed by this `DatabaseWriter`.
///
/// The ``Database`` argument to `updates` is valid only during the
/// execution of the closure. Do not store or return the database connection
/// for later use.
///
/// It is a programmer error to call this method from another database
/// access method. Doing so raises a "Database methods are not reentrant"
/// fatal error at runtime.
///
/// - warning: Database operations are not wrapped in a transaction. They
/// can see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `updates`
/// closure may not return the same value. Concurrent database accesses
/// can see partial updates performed by the `updates` closure.
///
/// - parameter updates: A closure which accesses the database.
/// - throws: The error thrown by `updates`.
public func inDatabase<T>(_ updates: (Database) throws -> T) rethrows -> T {
try writer.sync(updates)
}
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
try writer.reentrantSync(updates)
}
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
writer.async(updates)
}
}
// MARK: - Temp Copy
extension DatabaseQueue {
/// Returns a connection to an in-memory copy of the database at `path`.
///
/// Changes performed on the returned connection do not impact the
/// original database at `path`.
///
/// The database memory is released when the returned connection
/// is deallocated.
///
/// For example:
///
/// ```swift
/// let path = "/path/to/database.sqlite"
/// let dbQueue = try DatabaseQueue.inMemoryCopy(fromPath: path)
/// ```
public static func inMemoryCopy(
fromPath path: String,
configuration: Configuration = Configuration())
throws -> DatabaseQueue
{
var sourceConfig = configuration
sourceConfig.readonly = true
let source = try DatabaseQueue(path: path, configuration: sourceConfig)
var copyConfig = configuration
copyConfig.readonly = false
let result = try DatabaseQueue(configuration: copyConfig)
try source.backup(to: result)
if configuration.readonly {
// Result was not opened read-only so that we could perform the
// copy. And SQLITE_OPEN_READONLY has no effect on in-memory
// databases anyway.
//
// So let's simulate read-only with PRAGMA query_only.
try result.inDatabase { db in
try db.beginReadOnly()
}
}
return result
}
/// Returns a connection to a private, temporary, on-disk copy of the
/// database at `path`.
///
/// Changes performed on the returned connection do not impact the
/// original database at `path`.
///
/// The on-disk copy will be automatically deleted from disk as soon as
/// the returned connection is closed or deallocated.
///
/// For example:
///
/// ```swift
/// let path = "/path/to/database.sqlite"
/// let dbQueue = try DatabaseQueue.temporaryCopy(fromPath: path)
/// ```
public static func temporaryCopy(
fromPath path: String,
configuration: Configuration = Configuration())
throws -> DatabaseQueue
{
var sourceConfig = configuration
sourceConfig.readonly = true
let source = try DatabaseQueue(path: path, configuration: sourceConfig)
// <https://www.sqlite.org/c3ref/open.html>
// > If the filename is an empty string, then a private, temporary
// > on-disk database will be created. This private database will be
// > automatically deleted as soon as the database connection
// > is closed.
var copyConfig = configuration
copyConfig.readonly = false
let result = try DatabaseQueue(path: "", configuration: copyConfig)
try source.backup(to: result)
if configuration.readonly {
// Result was not opened read-only so that we could perform the
// copy. So let's simulate read-only with PRAGMA query_only.
try result.inDatabase { db in
try db.beginReadOnly()
}
}
return result
}
}
@@ -0,0 +1,764 @@
#if canImport(Combine)
import Combine
#endif
import Dispatch
/// A type that reads from an SQLite database.
///
/// Do not declare new conformances to `DatabaseReader`. Only the built-in
/// conforming types are valid.
///
/// The protocol comes with isolation guarantees that describe the behavior of
/// conforming types in a multithreaded application. See <doc:Concurrency> for
/// more information.
///
/// ## Topics
///
/// ### Database Information
///
/// - ``configuration``
/// - ``path``
///
/// ### Reading from the Database
///
/// - ``read(_:)-3806d``
/// - ``read(_:)-4w6gy``
/// - ``readPublisher(receiveOn:value:)``
/// - ``asyncRead(_:)``
///
/// ### Unsafe Methods
///
/// - ``unsafeRead(_:)-5i7tf``
/// - ``unsafeRead(_:)-11mk0``
/// - ``unsafeReentrantRead(_:)``
/// - ``asyncUnsafeRead(_:)``
///
/// ### Printing Database Content
///
/// - ``dumpContent(format:to:)``
/// - ``dumpRequest(_:format:to:)``
/// - ``dumpSchema(to:)``
/// - ``dumpSQL(_:format:to:)``
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
/// - ``DumpFormat``
/// - ``DumpTableHeaderOptions``
///
/// ### Other Database Operations
///
/// - ``backup(to:pagesPerStep:progress:)``
/// - ``close()``
/// - ``interrupt()``
///
/// ### Supporting Types
///
/// - ``AnyDatabaseReader``
public protocol DatabaseReader: AnyObject, Sendable {
/// The database configuration.
var configuration: Configuration { get }
/// The path to the database file.
///
/// In-memory databases also have a path:
/// see [In-Memory Databases](https://www.sqlite.org/inmemorydb.html).
var path: String { get }
/// Closes the database connection.
///
/// - note: You do not have to call this method, and you should not call
/// it unless the correct execution of your program depends on precise
/// database closing. Database connections are automatically closed when
/// they are deinitialized, and this is sufficient for most applications.
///
/// If this method does not throw, then the database is properly closed, and
/// every future database access will throw a ``DatabaseError`` of
/// code `SQLITE_MISUSE`.
///
/// Otherwise, there exists concurrent database accesses or living prepared
/// statements that prevent the database from closing, and this method
/// throws a ``DatabaseError`` of code `SQLITE_BUSY`.
/// See <https://www.sqlite.org/c3ref/close.html> for more information.
///
/// After an error has been thrown, the database may still be opened, and
/// you can keep on accessing it. It may also remain in a "zombie" state,
/// in which case it will throw `SQLITE_MISUSE` for all future
/// database accesses.
///
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
func close() throws
// MARK: - Interrupting Database Operations
/// Causes any pending database operation to abort and return at its
/// earliest opportunity.
///
/// This method can be called from any thread.
///
/// A call to `interrupt()` that occurs when there are no running SQL
/// statements is a no-op and has no effect on SQL statements that are
/// started after `interrupt()` returns.
///
/// A database operation that is interrupted will throw a ``DatabaseError``
/// with code `SQLITE_INTERRUPT`. If the interrupted SQL operation is an
/// `INSERT`, `UPDATE`, or `DELETE` that is inside an explicit transaction,
/// then the entire transaction will be rolled back automatically. If the
/// rolled back transaction was started by a transaction-wrapping method
/// such as ``DatabaseWriter/write(_:)-76inz`` or
/// ``Database/inTransaction(_:_:)``, then all database accesses will throw
/// a ``DatabaseError`` with code `SQLITE_ABORT` until the wrapping
/// method returns.
///
/// For example:
///
/// ```swift
/// try dbQueue.write { db in
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// // not executed:
/// try Player(...).insert(db)
/// } // throws SQLITE_INTERRUPT
///
/// try dbQueue.write { db in
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// try Player(...).insert(db) // throws SQLITE_ABORT
/// } // throws SQLITE_ABORT
///
/// try dbQueue.write { db in
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// } // throws SQLITE_ABORT
/// ```
///
/// Beware: when an application opens a transaction without a
/// transaction-wrapping method, no `SQLITE_ABORT` error warns of
/// aborted transactions:
///
/// ```swift
/// try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
/// try db.beginTransaction()
/// do {
/// // interrupted:
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
/// } catch { }
/// try Player(...).insert(db) // success
/// try db.commit() // throws SQLITE_ERROR "cannot commit - no transaction is active"
/// }
/// ```
///
/// Both `SQLITE_ABORT` and `SQLITE_INTERRUPT` errors can be checked with the
/// ``DatabaseError/isInterruptionError`` property.
func interrupt()
// MARK: - Read From Database
/// Executes read-only database operations, and returns their result after
/// they have finished executing.
///
/// For example:
///
/// ```swift
/// let count = try reader.read { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// It is a programmer error to call this method from another database
/// access method. Doing so raises a "Database methods are not reentrant"
/// fatal error at runtime.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
func read<T>(_ value: (Database) throws -> T) throws -> T
/// Schedules read-only database operations for execution, and
/// returns immediately.
///
/// For example:
///
/// ```swift
/// try reader.asyncRead { dbResult in
/// do {
/// let db = try dbResult.get()
/// let count = try Player.fetchCount(db)
/// } catch {
/// // Handle error
/// }
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// - parameter value: A closure which accesses the database. Its argument
/// is a `Result` that provides the database connection, or the failure
/// that would prevent establishing the read access to the database.
func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void)
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// For example:
///
/// ```swift
/// let count = try reader.unsafeRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// It is a programmer error to call this method from another database
/// access method. Doing so raises a "Database methods are not reentrant"
/// fatal error at runtime.
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T
/// Schedules database operations for execution, and returns immediately.
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// For example:
///
/// ```swift
/// reader.asyncUnsafeRead { dbResult in
/// do {
/// let db = try dbResult.get()
/// let count = try Player.fetchCount(db)
/// } catch {
/// // handle error
/// }
/// }
/// ```
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database. Its argument
/// is a `Result` that provides the database connection, or the failure
/// that would prevent establishing the read access to the database.
func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void)
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// This method can be called from other database access methods. If called
/// from the dispatch queue of a current database access (read or write),
/// the `Database` argument to `value` is the same as the current
/// database access.
///
/// Reentrant database accesses are discouraged because they muddle
/// transaction boundaries
/// (see <doc:Concurrency#Rule-2:-Mind-your-transactions> for
/// more information).
///
/// For example:
///
/// ```swift
/// let count = try reader.unsafeReentrantRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T
// MARK: - Value Observation
/// Starts a value observation.
///
/// Use the ``ValueObservation/start(in:scheduling:onError:onChange:)``
/// method instead.
///
/// - parameter observation: a ValueObservation.
/// - returns: A DatabaseCancellable that can stop the observation.
func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
}
extension DatabaseReader {
// MARK: - Backup
/// Copies the database contents into another database.
///
/// The `backup` method blocks the current thread until the destination
/// database contains the same contents as the source database.
///
/// When the source is a DatabasePool, concurrent writes can happen during
/// the backup. Those writes may, or may not, be reflected in the backup,
/// but they won't trigger any error.
///
/// Usage:
///
/// ```swift
/// let source: DatabaseQueue = ...
/// let destination: DatabaseQueue = ...
/// try source.backup(to: destination)
/// ```
///
/// When you're after progress reporting during backup, you'll want to
/// perform the backup in several steps. Each step copies the number of
/// _database pages_ you specify. See <https://www.sqlite.org/c3ref/backup_finish.html>
/// for more information:
///
/// ```swift
/// // Backup with progress reporting
/// try source.backup(to: destination, pagesPerStep: ...) { progress in
/// print("Database backup progress:", progress)
/// }
/// ```
///
/// The `progress` callback will be called at least oncewhen
/// `backupProgress.isCompleted == true`. If the callback throws
/// when `backupProgress.isCompleted == false`, the backup is aborted
/// and the error is rethrown. If the callback throws when
/// `backupProgress.isCompleted == true`, backup completion is
/// unaffected and the error is silently ignored.
///
/// See also ``Database/backup(to:pagesPerStep:progress:)``
///
/// - parameters:
/// - writer: The destination database.
/// - pagesPerStep: The number of database pages copied on each backup
/// step. By default, all pages are copied in one single step.
/// - progress: An optional function that is notified of the backup
/// progress.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs, or the
/// error thrown by `progress`.
public func backup(
to writer: some DatabaseWriter,
pagesPerStep: CInt = -1,
progress: ((DatabaseBackupProgress) throws -> Void)? = nil)
throws
{
try writer.writeWithoutTransaction { destDb in
try backup(
to: destDb,
pagesPerStep: pagesPerStep,
afterBackupStep: progress)
}
}
func backup(
to destDb: Database,
pagesPerStep: CInt = -1,
afterBackupInit: (() -> Void)? = nil,
afterBackupStep: ((DatabaseBackupProgress) throws -> Void)? = nil)
throws
{
try read { dbFrom in
try dbFrom.backupInternal(
to: destDb,
pagesPerStep: pagesPerStep,
afterBackupInit: afterBackupInit,
afterBackupStep: afterBackupStep)
}
}
}
extension DatabaseReader {
// MARK: - Asynchronous Database Access
/// Executes read-only database operations, and returns their result after
/// they have finished executing.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// For example:
///
/// ```swift
/// let count = try await reader.read { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func read<T>(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T {
try await withUnsafeThrowingContinuation { continuation in
asyncRead { result in
do {
try continuation.resume(returning: value(result.get()))
} catch {
continuation.resume(throwing: error)
}
}
}
}
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// This method is "unsafe" because the database reader does nothing more
/// than providing a database connection. When you use this method, you
/// become responsible for the thread-safety of your application, and
/// responsible for database accesses performed by other processes. See
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
/// more information.
///
/// For example:
///
/// ```swift
/// let count = try await reader.unsafeRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - warning: Database operations may not be wrapped in a transaction. They
/// may see changes performed by concurrent writes or writes performed by
/// other processes: two identical requests performed by the `value`
/// closure may not return the same value.
/// - warning: Attempts to write in the database may succeed.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func unsafeRead<T>(_ value: @Sendable @escaping (Database) throws -> T) async throws -> T {
try await withUnsafeThrowingContinuation { continuation in
asyncUnsafeRead { result in
do {
try continuation.resume(returning: value(result.get()))
} catch {
continuation.resume(throwing: error)
}
}
}
}
}
#if canImport(Combine)
extension DatabaseReader {
// MARK: - Publishing Database Values
/// Returns a publisher that publishes one value and completes.
///
/// The database is not accessed until subscription. Value and completion
/// are published on `scheduler` (the main dispatch queue by default).
///
/// For example:
///
/// ```swift
/// // DatabasePublishers.Read<Int>
/// let countPublisher = reader.readPublisher { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// Database operations are isolated in a transaction: they do not see
/// changes performed by eventual concurrent writes (even writes performed
/// by other processes).
///
/// The database connection is read-only: attempts to write throw a
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - parameter scheduler: A Combine Scheduler.
/// - parameter value: A closure which accesses the database.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func readPublisher<Output>(
receiveOn scheduler: some Combine.Scheduler = DispatchQueue.main,
value: @escaping (Database) throws -> Output)
-> DatabasePublishers.Read<Output>
{
Deferred {
Future { fulfill in
self.asyncRead { dbResult in
fulfill(dbResult.flatMap { db in Result { try value(db) } })
}
}
}
.receiveValues(on: scheduler)
.eraseToReadPublisher()
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension DatabasePublishers {
/// A publisher that reads from the database.
///
/// `Read` publishes exactly one element, or an error.
///
/// You build such a publisher from ``DatabaseReader``.
public struct Read<Output>: Publisher {
public typealias Output = Output
public typealias Failure = Error
fileprivate let upstream: AnyPublisher<Output, Error>
public func receive<S>(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
upstream.receive(subscriber: subscriber)
}
}
}
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension Publisher where Failure == Error {
fileprivate func eraseToReadPublisher() -> DatabasePublishers.Read<Output> {
.init(upstream: eraseToAnyPublisher())
}
}
#endif
extension DatabaseReader {
// MARK: - Value Observation Support
/// Adding an observation in a read-only database emits only the
/// initial value.
func _addReadOnly<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
if scheduler.immediateInitialValue() {
do {
// Perform a reentrant read, in case the observation would be
// started from a database access.
let value = try unsafeReentrantRead { db in
try db.isolated(readOnly: true) {
try observation.fetchInitialValue(db)
}
}
onChange(value)
} catch {
observation.events.didFail?(error)
}
return AnyDatabaseCancellable(cancel: { /* nothing to cancel */ })
} else {
var isCancelled = false
asyncRead { dbResult in
guard !isCancelled else { return }
let result = dbResult.flatMap { db in
Result { try observation.fetchInitialValue(db) }
}
scheduler.schedule {
guard !isCancelled else { return }
do {
try onChange(result.get())
} catch {
observation.events.didFail?(error)
}
}
}
return AnyDatabaseCancellable(cancel: { isCancelled = true })
}
}
}
/// A type-erased database reader.
///
/// An instance of `AnyDatabaseReader` forwards its operations to an underlying
/// base database reader.
public final class AnyDatabaseReader {
private let base: any DatabaseReader
/// Creates a new database reader that wraps and forwards operations
/// to `base`.
public init(_ base: some DatabaseReader) {
self.base = base
}
}
extension AnyDatabaseReader: DatabaseReader {
public var configuration: Configuration {
base.configuration
}
public var path: String {
base.path
}
public func close() throws {
try base.close()
}
public func interrupt() {
base.interrupt()
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func read<T>(_ value: (Database) throws -> T) throws -> T {
try base.read(value)
}
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
base.asyncRead(value)
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
try base.unsafeRead(value)
}
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
base.asyncUnsafeRead(value)
}
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
try base.unsafeReentrantRead(value)
}
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
base._add(
observation: observation,
scheduling: scheduler,
onChange: onChange)
}
}
/// A type that sees an unchanging database content.
///
/// Do not declare new conformances to `DatabaseSnapshotReader`. Only the
/// built-in conforming types are valid.
///
/// The protocol comes with the same features and guarantees as
/// ``DatabaseReader``. On top of them, a `DatabaseSnapshotReader` always sees
/// the same state of the database.
///
/// ## Topics
///
/// ### Reading from the Database
///
/// - ``reentrantRead(_:)``
public protocol DatabaseSnapshotReader: DatabaseReader { }
extension DatabaseSnapshotReader {
/// Executes database operations, and returns their result after they have
/// finished executing.
///
/// This method can be called from other database access methods. If called
/// from the dispatch queue of a current database access, the `Database`
/// argument to `value` is the same as the current database access.
///
/// For example:
///
/// ```swift
/// let count = try snapshot.reentrantRead { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// The ``Database`` argument to `value` is valid only during the execution
/// of the closure. Do not store or return the database connection for
/// later use.
///
/// - parameter value: A closure which accesses the database.
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
/// would happen while establishing the database access.
public func reentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
// Reentrant reads are safe in a snapshot
try unsafeReentrantRead(value)
}
// There is no such thing as an unsafe access to a snapshot.
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
try read(value)
}
// There is no such thing as an unsafe access to a snapshot.
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
asyncRead(value)
}
}
@@ -0,0 +1,471 @@
/// An observable region of the database.
///
/// A `DatabaseRegion` is the union of any number of "table regions", which can
/// cover a full table, or the combination of columns and rows identified by
/// their rowids:
///
/// |Table1 | |Table2 | |Table3 | |Table4 | |Table5 |
/// |-------| |-------| |-------| |-------| |-------|
/// |x|x|x|x| |x| | | | |x|x|x|x| |x|x| |x| | | | | |
/// |x|x|x|x| |x| | | | | | | | | | | | | | | |x| | |
/// |x|x|x|x| |x| | | | | | | | | |x|x| |x| | | | | |
/// |x|x|x|x| |x| | | | | | | | | | | | | | | | | | |
///
/// It is dedicated to help ``TransactionObserver`` types detect impactful
/// database changes.
///
/// You get `DatabaseRegion` instances from a ``DatabaseRegionConvertible``
/// value, a prepared ``Statement``, or from the initializers described below.
///
/// ## Topics
///
/// ### Creating Regions
///
/// - ``fullDatabase-3ir3p``
/// - ``init()``
///
/// ### Instance Properties
///
/// - ``isEmpty``
/// - ``isFullDatabase``
///
/// ### Combining Regions
///
/// - ``formUnion(_:)``
/// - ``union(_:)``
///
/// ### Detecting Database Changes
///
/// Use those methods from ``TransactionObserver`` methods.
///
/// - ``isModified(byEventsOfKind:)``
/// - ``isModified(by:)``
public struct DatabaseRegion: Sendable {
private let tableRegions: [CaseInsensitiveIdentifier: TableRegion]?
private init(tableRegions: [CaseInsensitiveIdentifier: TableRegion]?) {
self.tableRegions = tableRegions
}
/// Returns whether the region is empty.
public var isEmpty: Bool {
guard let tableRegions else {
// full database
return false
}
return tableRegions.isEmpty
}
/// Returns whether the region covers the full database.
public var isFullDatabase: Bool {
tableRegions == nil
}
/// The region that covers the full database.
public static let fullDatabase = DatabaseRegion(tableRegions: nil)
/// The empty database region.
public init() {
self.init(tableRegions: [:])
}
/// Creates a region that spans all rows and columns of a database table.
///
/// - parameter table: A table name.
init(table: String) {
let table = CaseInsensitiveIdentifier(rawValue: table)
self.init(tableRegions: [table: TableRegion(columns: nil, rowIds: nil)])
}
/// Full columns in a table: (some columns in a table) × (all rows)
init(table: String, columns: Set<String>) {
let table = CaseInsensitiveIdentifier(rawValue: table)
let columns = Set(columns.map(CaseInsensitiveIdentifier.init))
self.init(tableRegions: [table: TableRegion(columns: columns, rowIds: nil)])
}
/// Full rows in a table: (all columns in a table) × (some rows)
init(table: String, rowIds: Set<Int64>) {
let table = CaseInsensitiveIdentifier(rawValue: table)
self.init(tableRegions: [table: TableRegion(columns: nil, rowIds: rowIds)])
}
/// Returns the intersection of this region and the given one.
///
/// This method is not public because there is no known public use case for
/// this intersection. It is currently only used as support for
/// the isModified(byEventsOfKind:) method.
func intersection(_ other: DatabaseRegion) -> DatabaseRegion {
guard let tableRegions else { return other }
guard let otherTableRegions = other.tableRegions else { return self }
var tableRegionsIntersection: [CaseInsensitiveIdentifier: TableRegion] = [:]
for (table, tableRegion) in tableRegions {
guard let otherTableRegion = otherTableRegions
.first(where: { (otherTable, _) in otherTable == table })?
.value else { continue }
let tableRegionIntersection = tableRegion.intersection(otherTableRegion)
guard !tableRegionIntersection.isEmpty else { continue }
tableRegionsIntersection[table] = tableRegionIntersection
}
return DatabaseRegion(tableRegions: tableRegionsIntersection)
}
/// Only keeps those rowIds in the given table
func tableIntersection(_ table: String, rowIds: Set<Int64>) -> DatabaseRegion {
guard var tableRegions else {
return DatabaseRegion(table: table, rowIds: rowIds)
}
let table = CaseInsensitiveIdentifier(rawValue: table)
guard let tableRegion = tableRegions[table] else {
return self
}
let intersection = tableRegion.intersection(TableRegion(columns: nil, rowIds: rowIds))
if intersection.isEmpty {
tableRegions.removeValue(forKey: table)
} else {
tableRegions[table] = intersection
}
return DatabaseRegion(tableRegions: tableRegions)
}
/// Returns the union of this region and the given one.
public func union(_ other: DatabaseRegion) -> DatabaseRegion {
guard let tableRegions else { return .fullDatabase }
guard let otherTableRegions = other.tableRegions else { return .fullDatabase }
var tableRegionsUnion: [CaseInsensitiveIdentifier: TableRegion] = [:]
let tableNames = Set(tableRegions.keys).union(Set(otherTableRegions.keys))
for table in tableNames {
let tableRegion = tableRegions[table]
let otherTableRegion = otherTableRegions[table]
let tableRegionUnion: TableRegion
switch (tableRegion, otherTableRegion) {
case (nil, nil):
preconditionFailure()
case let (nil, tableRegion?), let (tableRegion?, nil):
tableRegionUnion = tableRegion
case let (tableRegion?, otherTableRegion?):
tableRegionUnion = tableRegion.union(otherTableRegion)
}
tableRegionsUnion[table] = tableRegionUnion
}
return DatabaseRegion(tableRegions: tableRegionsUnion)
}
/// Inserts the given region into this region
public mutating func formUnion(_ other: DatabaseRegion) {
self = union(other)
}
/// Returns a region suitable for database observation
func observableRegion(_ db: Database) throws -> DatabaseRegion {
// SQLite does not expose schema changes to the
// TransactionObserver protocol. By removing internal SQLite tables from
// the observed region, we optimize database observation.
//
// And by canonicalizing table names, we remove views, and help the
// `isModified` methods. (TODO: is this comment still accurate?
// Isn't it about providing TransactionObserver.observes() with
// real tables names, instead?)
try ignoringInternalSQLiteTables().canonicalTables(db)
}
/// Returns a region only made of actual tables with their canonical names.
///
/// This method removes views.
func canonicalTables(_ db: Database) throws -> DatabaseRegion {
guard let tableRegions else { return .fullDatabase }
var region = DatabaseRegion()
for (table, tableRegion) in tableRegions {
if let canonicalTableName = try db.canonicalTableName(table.rawValue) {
let table = CaseInsensitiveIdentifier(rawValue: canonicalTableName)
region.formUnion(DatabaseRegion(tableRegions: [table: tableRegion]))
}
}
return region
}
/// Returns a region which doesn't contain any SQLite internal table.
private func ignoringInternalSQLiteTables() -> DatabaseRegion {
guard let tableRegions else { return .fullDatabase }
let filteredRegions = tableRegions.filter {
!Database.isSQLiteInternalTable($0.key.rawValue)
}
return DatabaseRegion(tableRegions: filteredRegions)
}
}
extension DatabaseRegion {
// MARK: - Database Events
/// Returns whether the content in the region would be impacted if the
/// database were modified by an event of this kind.
public func isModified(byEventsOfKind eventKind: DatabaseEventKind) -> Bool {
intersection(eventKind.modifiedRegion).isEmpty == false
}
/// Returns whether the content in the region is impacted by this event.
///
/// - precondition: event has been filtered by the same region
/// in the TransactionObserver.observes(eventsOfKind:) method, by calling
/// region.isModified(byEventsOfKind:)
public func isModified(by event: DatabaseEvent) -> Bool {
guard let tableRegions else {
// Full database: all changes are impactful
return true
}
guard let tableRegion = tableRegions[CaseInsensitiveIdentifier(rawValue: event.tableName)] else {
// FTS4 (and maybe other virtual tables) perform unadvertised
// changes. For example, an "INSERT INTO document ..." statement
// advertises an insertion in the `document` table, but the
// actual change events happen in the `document_content` shadow
// table. When such a non-advertised event happens, assume that
// the region is modified.
// See https://github.com/groue/GRDB.swift/issues/620
return true
}
return tableRegion.contains(rowID: event.rowID)
}
/// Returns an array of all event kinds that can impact this region.
///
/// - precondition: the region is canonical.
func impactfulEventKinds(_ db: Database) throws -> [DatabaseEventKind] {
if let tableRegions {
return try tableRegions.flatMap { (table, tableRegion) -> [DatabaseEventKind] in
let tableName = table.rawValue // canonical table name
let columnNames: Set<String>
if let columns = tableRegion.columns {
columnNames = Set(columns.map(\.rawValue))
} else {
columnNames = try Set(db.columns(in: tableName).map(\.name))
}
return [
DatabaseEventKind.delete(tableName: tableName),
DatabaseEventKind.insert(tableName: tableName),
DatabaseEventKind.update(tableName: tableName, columnNames: columnNames),
]
}
} else {
// full database
return try db.schemaIdentifiers().flatMap { schemaIdentifier in
let schema = try db.schema(schemaIdentifier)
return try schema.objects
.filter { $0.type == .table }
.flatMap { table in
let columnNames = try Set(db.columns(in: table.name).map(\.name))
return [
DatabaseEventKind.delete(tableName: table.name),
DatabaseEventKind.insert(tableName: table.name),
DatabaseEventKind.update(tableName: table.name, columnNames: columnNames),
]
}
}
}
}
}
extension DatabaseRegion: Equatable {
public static func == (lhs: DatabaseRegion, rhs: DatabaseRegion) -> Bool {
switch (lhs.tableRegions, rhs.tableRegions) {
case (nil, nil):
return true
case let (ltableRegions?, rtableRegions?):
let ltableNames = Set(ltableRegions.keys)
let rtableNames = Set(rtableRegions.keys)
guard ltableNames == rtableNames else {
return false
}
for tableName in ltableNames where ltableRegions[tableName]! != rtableRegions[tableName]! {
return false
}
return true
default:
return false
}
}
}
extension DatabaseRegion: CustomStringConvertible {
public var description: String {
guard let tableRegions else {
return "full database"
}
if tableRegions.isEmpty {
return "empty"
}
return tableRegions
.sorted(by: { (l, r) in l.key.rawValue < r.key.rawValue })
.map { (table, tableRegion) in
var desc = table.rawValue
if let columns = tableRegion.columns {
desc += "(" + columns.map(\.rawValue).sorted().joined(separator: ",") + ")"
} else {
desc += "(*)"
}
if let rowIds = tableRegion.rowIds {
desc += "[" + rowIds.sorted().map { "\($0)" }.joined(separator: ",") + "]"
}
return desc
}
.joined(separator: ",")
}
}
private struct TableRegion: Equatable {
var columns: Set<CaseInsensitiveIdentifier>? // nil means "all columns"
var rowIds: Set<Int64>? // nil means "all rowids"
var isEmpty: Bool {
if let columns, columns.isEmpty { return true }
if let rowIds, rowIds.isEmpty { return true }
return false
}
func intersection(_ other: TableRegion) -> TableRegion {
let columnsIntersection: Set<CaseInsensitiveIdentifier>?
switch (self.columns, other.columns) {
case let (nil, columns), let (columns, nil):
columnsIntersection = columns
case let (columns?, other?):
columnsIntersection = columns.intersection(other)
}
let rowIdsIntersection: Set<Int64>?
switch (self.rowIds, other.rowIds) {
case let (nil, rowIds), let (rowIds, nil):
rowIdsIntersection = rowIds
case let (rowIds?, other?):
rowIdsIntersection = rowIds.intersection(other)
}
return TableRegion(columns: columnsIntersection, rowIds: rowIdsIntersection)
}
func union(_ other: TableRegion) -> TableRegion {
let columnsUnion: Set<CaseInsensitiveIdentifier>?
switch (self.columns, other.columns) {
case (nil, _), (_, nil):
columnsUnion = nil
case let (columns?, other?):
columnsUnion = columns.union(other)
}
let rowIdsUnion: Set<Int64>?
switch (self.rowIds, other.rowIds) {
case (nil, _), (_, nil):
rowIdsUnion = nil
case let (rowIds?, other?):
rowIdsUnion = rowIds.union(other)
}
return TableRegion(columns: columnsUnion, rowIds: rowIdsUnion)
}
func contains(rowID: Int64) -> Bool {
guard let rowIds else {
return true
}
return rowIds.contains(rowID)
}
}
// MARK: - DatabaseRegionConvertible
/// A type that operates on a specific ``DatabaseRegion``.
///
/// A `DatabaseRegionConvertible` instance feeds database observation tools such
/// as ``DatabaseRegionObservation`` and ``ValueObservation``:
///
/// ```swift
/// // An observation triggered by all changes to the database
/// DatabaseRegionObservation(tracking: .fullDatabase)
///
/// // An observation triggered by all changes to the 'player' table
/// DatabaseRegionObservation(tracking: Table("player"))
///
/// // An observation triggered by all changes to the row
/// // with rowid 1 in the 'player' table
/// DatabaseRegionObservation(tracking: Player.filter(id: 1))
///
/// // An observation triggered by all changes to the 'score' column
/// // of the 'player' table
/// DatabaseRegionObservation(tracking: SQLRequest("SELECT score FROM player"))
/// ```
///
/// Specifying a region from a ``FetchRequest`` does not execute the request.
/// In the above example, `Player.filter(id: 1)` and `SELECT score FROM player`
/// are only compiled by SQLite, so that GRDB can understand the tables, rows,
/// and columns that constitute the database region.
///
/// ## Topics
///
/// ### Creating a DatabaseRegion
///
/// - ``fullDatabase``
/// - ``databaseRegion(_:)``
///
/// ### Supporting Types
///
/// - ``AnyDatabaseRegionConvertible``
public protocol DatabaseRegionConvertible {
/// Returns a database region.
///
/// - parameter db: A database connection.
func databaseRegion(_ db: Database) throws -> DatabaseRegion
}
extension DatabaseRegionConvertible where Self == DatabaseRegion {
/// The region that covers the full database: all columns and all rows
/// from all tables.
public static var fullDatabase: Self { DatabaseRegion.fullDatabase }
}
extension DatabaseRegion: DatabaseRegionConvertible {
public func databaseRegion(_ db: Database) throws -> DatabaseRegion {
self
}
}
/// A type-erased DatabaseRegionConvertible
public struct AnyDatabaseRegionConvertible: DatabaseRegionConvertible {
let _region: (Database) throws -> DatabaseRegion
public init(_ region: @escaping (Database) throws -> DatabaseRegion) {
_region = region
}
public init(_ region: some DatabaseRegionConvertible) {
_region = region.databaseRegion
}
public func databaseRegion(_ db: Database) throws -> DatabaseRegion {
try _region(db)
}
}
// MARK: - Utils
extension DatabaseRegion {
static func union(_ regions: DatabaseRegion...) -> DatabaseRegion {
regions.reduce(into: DatabaseRegion()) { union, region in
union.formUnion(region)
}
}
static func union(_ regions: [any DatabaseRegionConvertible]) -> (Database) throws -> DatabaseRegion {
return { db in
try regions.reduce(into: DatabaseRegion()) { union, region in
try union.formUnion(region.databaseRegion(db))
}
}
}
}
@@ -0,0 +1,316 @@
#if canImport(Combine)
import Combine
#endif
import Foundation
public struct DatabaseRegionObservation {
/// A closure that is evaluated when the observation starts, and returns
/// the observed database region.
var observedRegion: (Database) throws -> DatabaseRegion
}
extension DatabaseRegionObservation {
/// Creates a `DatabaseRegionObservation` that notifies all transactions
/// that modify one of the provided regions.
///
/// For example:
///
/// ```swift
/// // An observation that tracks the 'player' table
/// let observation = DatabaseRegionObservation(tracking: Player.all())
/// ```
///
/// - parameter regions: A list of observed regions.
public init(tracking regions: any DatabaseRegionConvertible...) {
self.init(tracking: regions)
}
/// Creates a `DatabaseRegionObservation` that notifies all transactions
/// that modify one of the provided regions.
///
/// For example:
///
/// ```swift
/// // An observation that tracks the 'player' table
/// let observation = DatabaseRegionObservation(tracking: [Player.all()])
/// ```
///
/// - parameter regions: An array of observed regions.
public init(tracking regions: [any DatabaseRegionConvertible]) {
self.init(observedRegion: DatabaseRegion.union(regions))
}
}
extension DatabaseRegionObservation {
/// The state of a started DatabaseRegionObservation
private enum ObservationState {
case cancelled
case pending
case started(DatabaseRegionObserver)
}
/// Starts observing the database.
///
/// The observation lasts until the returned cancellable is cancelled
/// or deallocated.
///
/// For example:
///
/// ```swift
/// let observation = DatabaseRegionObservation(tracking: Player.all())
///
/// let cancellable = try observation.start(in: dbQueue) { error in
/// // handle error
/// } onChange: { (db: Database) in
/// print("A modification of the player table has just been committed.")
/// }
/// ```
///
/// If this method is called from the writer dispatch queue of `writer` (see
/// ``DatabaseWriter``), the observation starts immediately. Otherwise, it
/// blocks the current thread until a write access can be established.
///
/// Both `onError` and `onChange` closures are executed in the writer
/// dispatch queue, serialized with all database updates performed
/// by `writer`.
///
/// The ``Database`` argument to `onChange` is valid only during the
/// execution of the closure. Do not store or return the database connection
/// for later use.
///
/// - parameter writer: A DatabaseWriter.
/// - parameter onError: The closure to execute when the observation fails.
/// - parameter onChange: The closure to execute when a transaction has
/// modified the observed region.
/// - returns: A DatabaseCancellable that can stop the observation.
public func start(
in writer: some DatabaseWriter,
onError: @escaping (Error) -> Void,
onChange: @escaping (Database) -> Void)
-> AnyDatabaseCancellable
{
@LockedBox var state = ObservationState.pending
// Use unsafeReentrantWrite so that observation can start from any
// dispatch queue.
writer.unsafeReentrantWrite { db in
do {
let region = try observedRegion(db).observableRegion(db)
$state.update {
let observer = DatabaseRegionObserver(region: region, onChange: {
if case .cancelled = state {
return
}
onChange($0)
})
// Use the `.observerLifetime` extent so that we can cancel
// the observation by deallocating the observer. This is
// a simpler way to cancel the observation than waiting for
// *another* write access in order to explicitly remove
// the observer.
db.add(transactionObserver: observer, extent: .observerLifetime)
$0 = .started(observer)
}
} catch {
onError(error)
}
}
return AnyDatabaseCancellable {
// Deallocates the transaction observer. This makes sure that the
// `onChange` callback will never be called again, because the
// observation was started with the `.observerLifetime` extent.
state = .cancelled
}
}
}
#if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension DatabaseRegionObservation {
// MARK: - Publishing Impactful Transactions
/// Returns a publisher that observes the database.
///
/// The publisher publishes ``Database`` connections on the writer dispatch
/// queue of `writer` (see ``DatabaseWriter``). Those connections are valid
/// only when published. Do not store or return them for later use.
///
/// Do not reschedule the publisher with `receive(on:options:)` or any
/// `Publisher` method that schedules publisher elements.
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
public func publisher(in writer: some DatabaseWriter) -> DatabasePublishers.DatabaseRegion {
DatabasePublishers.DatabaseRegion(self, in: writer)
}
}
#endif
private class DatabaseRegionObserver: TransactionObserver {
let region: DatabaseRegion
let onChange: (Database) -> Void
var isChanged = false
init(region: DatabaseRegion, onChange: @escaping (Database) -> Void) {
self.region = region
self.onChange = onChange
}
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
region.isModified(byEventsOfKind: eventKind)
}
func databaseDidChange() {
isChanged = true
stopObservingDatabaseChangesUntilNextTransaction()
}
func databaseDidChange(with event: DatabaseEvent) {
if region.isModified(by: event) {
isChanged = true
stopObservingDatabaseChangesUntilNextTransaction()
}
}
func databaseDidCommit(_ db: Database) {
guard isChanged else { return }
isChanged = false
onChange(db)
}
func databaseDidRollback(_ db: Database) {
isChanged = false
}
}
#if canImport(Combine)
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
extension DatabasePublishers {
/// A publisher that tracks transactions that modify a database region.
///
/// You build such a publisher from ``DatabaseRegionObservation``.
public struct DatabaseRegion: Publisher {
public typealias Output = Database
public typealias Failure = Error
let writer: any DatabaseWriter
let observation: DatabaseRegionObservation
init(_ observation: DatabaseRegionObservation, in writer: some DatabaseWriter) {
self.writer = writer
self.observation = observation
}
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = DatabaseRegionSubscription(
writer: writer,
observation: observation,
downstream: subscriber)
subscriber.receive(subscription: subscription)
}
}
private class DatabaseRegionSubscription<Downstream: Subscriber>: Subscription
where Downstream.Failure == Error, Downstream.Input == Database
{
private struct WaitingForDemand {
let downstream: Downstream
let writer: any DatabaseWriter
let observation: DatabaseRegionObservation
}
private struct Observing {
let downstream: Downstream
let writer: any DatabaseWriter // Retain writer until subscription is finished
var remainingDemand: Subscribers.Demand
}
private enum State {
// Waiting for demand, not observing the database.
case waitingForDemand(WaitingForDemand)
// Observing the database.
case observing(Observing)
// Completed or cancelled, not observing the database.
case finished
}
// cancellable is not stored in self.state because we must enter the
// .observing state *before* the observation starts.
private var cancellable: AnyDatabaseCancellable?
private var state: State
private var lock = NSRecursiveLock() // Allow re-entrancy
init(
writer: some DatabaseWriter,
observation: DatabaseRegionObservation,
downstream: Downstream)
{
state = .waitingForDemand(WaitingForDemand(
downstream: downstream,
writer: writer,
observation: observation))
}
func request(_ demand: Subscribers.Demand) {
lock.synchronized {
switch state {
case let .waitingForDemand(info):
guard demand > 0 else {
return
}
state = .observing(Observing(
downstream: info.downstream,
writer: info.writer,
remainingDemand: demand))
cancellable = info.observation.start(
in: info.writer,
onError: { [weak self] in self?.receive(failure: $0) },
onChange: { [weak self] in self?.receive($0) })
case var .observing(info):
info.remainingDemand += demand
state = .observing(info)
case .finished:
break
}
}
}
func cancel() {
lock.synchronized {
cancellable = nil
state = .finished
}
}
private func receive(_ value: Database) {
lock.synchronized {
if case let .observing(info) = state,
info.remainingDemand > .none
{
let additionalDemand = info.downstream.receive(value)
if case var .observing(info) = state {
info.remainingDemand += additionalDemand
info.remainingDemand -= 1
state = .observing(info)
}
}
}
}
private func receive(failure error: Error) {
lock.synchronized {
if case let .observing(info) = state {
state = .finished
info.downstream.receive(completion: .failure(error))
}
}
}
}
}
#endif
@@ -0,0 +1,78 @@
/// A thread-unsafe database schema cache
struct DatabaseSchemaCache {
/// A cached value
///
/// We cache both hits and misses, because we often query both the temp
/// schema and the main schema: not remembering misses would have us
/// perform too many database queries.
enum Presence<T> {
/// Value does not exist in the schema.
case missing
/// Value exists in the schema.
case value(T)
var value: T? {
switch self {
case .missing: return nil
case let .value(value): return value
}
}
}
var schemaInfo: SchemaInfo?
private var tables: [String: Presence<TableInfo>] = [:]
private var primaryKeys: [String: Presence<PrimaryKeyInfo>] = [:]
private var columns: [String: Presence<[ColumnInfo]>] = [:]
private var indexes: [String: Presence<[IndexInfo]>] = [:]
private var foreignKeys: [String: Presence<[ForeignKeyInfo]>] = [:]
mutating func clear() {
tables = [:]
primaryKeys = [:]
columns = [:]
indexes = [:]
foreignKeys = [:]
schemaInfo = nil
}
func table(_ table: String) -> Presence<TableInfo>? {
tables[table]
}
mutating func set(tableInfo: Presence<TableInfo>, forTable table: String) {
tables[table] = tableInfo
}
func primaryKey(_ table: String) -> Presence<PrimaryKeyInfo>? {
primaryKeys[table]
}
mutating func set(primaryKey: Presence<PrimaryKeyInfo>, forTable table: String) {
primaryKeys[table] = primaryKey
}
func columns(in table: String) -> Presence<[ColumnInfo]>? {
columns[table]
}
mutating func set(columns: Presence<[ColumnInfo]>, forTable table: String) {
self.columns[table] = columns
}
func indexes(on table: String) -> Presence<[IndexInfo]>? {
indexes[table]
}
mutating func set(indexes: Presence<[IndexInfo]>, forTable table: String) {
self.indexes[table] = indexes
}
func foreignKeys(on table: String) -> Presence<[ForeignKeyInfo]>? {
foreignKeys[table]
}
mutating func set(foreignKeys: Presence<[ForeignKeyInfo]>, forTable table: String) {
self.foreignKeys[table] = foreignKeys
}
}
@@ -0,0 +1,186 @@
import Dispatch
/// A database connection that serializes accesses to an unchanging
/// database content, as it existed at the moment the snapshot was created.
///
/// ## Overview
///
/// A `DatabaseSnapshot` never sees any database modification during all its
/// lifetime. All database accesses performed from a snapshot always see the
/// same identical database content.
///
/// A snapshot creates one single SQLite connection. All database
/// accesses are executed in a serial **reader dispatch queue**. The SQLite
/// connection is closed when the `DatabaseSnapshot` is deallocated.
///
/// A snapshot created on a [WAL](https://sqlite.org/wal.html) database doesn't
/// prevent database modifications performed by other connections (but it won't
/// see them). Refer to [Isolation In SQLite](https://sqlite.org/isolation.html)
/// for more information.
///
/// On non-WAL databases, a snapshot prevents all database modifications as long
/// as it exists, because of the
/// [SHARED lock](https://www.sqlite.org/lockingv3.html) it holds.
///
/// ## Usage
///
/// You create instances of `DatabaseSnapshot` from a ``DatabasePool``,
/// with ``DatabasePool/makeSnapshot()``:
///
/// ```swift
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
/// let snapshot = try dbPool.makeSnapshot()
/// let playerCount = try snapshot.read { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// When you want to control the database state seen by a snapshot, create the
/// snapshot from within a write access, outside of any transaction.
///
/// For example, compare the two snapshots below. The first one is guaranteed to
/// see an empty table of players, because is is created after all players have
/// been deleted, and from the serialized writer dispatch queue which prevents
/// any concurrent write. The second is created without this concurrency
/// protection, which means that some other threads may already have created
/// some players:
///
/// ```swift
/// let snapshot1 = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshot in
/// try db.inTransaction {
/// try Player.deleteAll()
/// return .commit
/// }
///
/// return try dbPool.makeSnapshot()
/// }
///
/// // <- Other threads may have created some players here
/// let snapshot2 = try dbPool.makeSnapshot()
///
/// // Guaranteed to be zero
/// let count1 = try snapshot1.read { db in
/// try Player.fetchCount(db)
/// }
///
/// // Could be anything
/// let count2 = try snapshot2.read { db in
/// try Player.fetchCount(db)
/// }
/// ```
///
/// `DatabaseSnapshot` inherits its database access methods from the
/// ``DatabaseReader`` protocols.
///
/// `DatabaseSnapshot` serializes database accesses and can't perform concurrent
/// reads. For concurrent reads, see ``DatabaseSnapshotPool``.
public final class DatabaseSnapshot {
private let reader: SerializedDatabase
public var configuration: Configuration {
reader.configuration
}
/// The path to the database file.
public var path: String {
reader.path
}
init(
path: String,
configuration: Configuration,
defaultLabel: String = "GRDB.DatabaseSnapshot",
purpose: String? = nil)
throws
{
let configuration = Self.configure(configuration)
reader = try SerializedDatabase(
path: path,
configuration: configuration,
defaultLabel: defaultLabel,
purpose: purpose)
try reader.sync { db in
// Open transaction
try db.beginTransaction(.deferred)
// Acquire snapshot isolation
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
}
}
deinit {
// Leave snapshot isolation
reader.reentrantSync { db in
try? db.commit()
}
}
private static func configure(_ configuration: Configuration) -> Configuration {
var configuration = configuration
// DatabaseSnapshot can't perform parallel reads.
configuration.maximumReaderCount = 1
// DatabaseSnapshot is read-only.
configuration.readonly = true
// DatabaseSnapshot uses deferred transactions by default.
// Other transaction kinds are forbidden by SQLite in read-only connections.
configuration.defaultTransactionKind = .deferred
// DatabaseSnapshot keeps a long-lived transaction.
configuration.allowsUnsafeTransactions = true
return configuration
}
}
extension DatabaseSnapshot: DatabaseSnapshotReader {
public func close() throws {
try reader.sync { try $0.close() }
}
// MARK: - Interrupting Database Operations
public func interrupt() {
reader.interrupt()
}
// MARK: - Reading from Database
public func read<T>(_ block: (Database) throws -> T) rethrows -> T {
try reader.sync(block)
}
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
reader.async { value(.success($0)) }
}
public func unsafeRead<T>(_ value: (Database) throws -> T) rethrows -> T {
try reader.sync(value)
}
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
reader.async { value(.success($0)) }
}
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
try reader.reentrantSync(value)
}
// MARK: - Database Observation
public func _add<Reducer: ValueReducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable
{
_addReadOnly(
observation: observation,
scheduling: scheduler,
onChange: onChange)
}
}
@@ -0,0 +1,382 @@
// swiftlint:disable:next line_length
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
/// A database connection that allows concurrent accesses to an unchanging
/// database content, as it existed at the moment the snapshot was created.
///
/// ## Overview
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// A `DatabaseSnapshotPool` never sees any database modification during all its
/// lifetime. All database accesses performed from a snapshot always see the
/// same identical database content.
///
/// It creates a pool of up to ``Configuration/maximumReaderCount`` read-only
/// SQLite connections. All read accesses are executed in **reader dispatch
/// queues** (one per read-only SQLite connection). SQLite connections are
/// closed when the `DatabasePool` is deallocated.
///
/// An SQLite database in the [WAL mode](https://www.sqlite.org/wal.html) is
/// required for creating a `DatabaseSnapshotPool`.
///
/// ## Usage
///
/// You create a `DatabaseSnapshotPool` from a
/// [WAL mode](https://www.sqlite.org/wal.html) database, such as databases
/// created from a ``DatabasePool``:
///
/// ```swift
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
/// let snapshot = try dbPool.makeSnapshotPool()
/// ```
///
/// When you want to control the database state seen by a snapshot, create the
/// snapshot from a database connection, outside of a write transaction. You can
/// for example take snapshots from a ``ValueObservation``:
///
/// ```swift
/// // An observation of the 'player' table
/// // that notifies fresh database snapshots:
/// let observation = ValueObservation.tracking { db in
/// // Don't fetch players now, and return a snapshot instead.
/// // Register an access to the player table so that the
/// // observation tracks changes to this table.
/// try db.registerAccess(to: Player.all())
/// return try DatabaseSnapshotPool(db)
/// }
///
/// // Start observing the 'player' table
/// let cancellable = try observation.start(in: dbPool) { error in
/// // Handle error
/// } onChange: { (snapshot: DatabaseSnapshotPool) in
/// // Handle a fresh snapshot
/// }
/// ```
///
/// `DatabaseSnapshotPool` inherits its database access methods from the
/// ``DatabaseReader`` protocols.
///
/// Related SQLite documentation:
///
/// - <https://www.sqlite.org/c3ref/snapshot_get.html>
/// - <https://www.sqlite.org/c3ref/snapshot_open.html>
///
/// ## Topics
///
/// ### Creating a DatabaseSnapshotPool
///
/// See also ``DatabasePool/makeSnapshotPool()``.
///
/// - ``init(_:configuration:)``
/// - ``init(path:configuration:)``
public final class DatabaseSnapshotPool {
public let configuration: Configuration
/// The path to the database file.
public let path: String
/// The pool of reader connections.
/// It is constant, until close() sets it to nil.
private var readerPool: Pool<SerializedDatabase>?
/// The WAL snapshot
private let walSnapshot: WALSnapshot
/// A connection that prevents checkpoints and keeps the WAL snapshot valid.
/// It is never used.
private let snapshotHolder: DatabaseQueue
/// Creates a snapshot of the database.
///
/// For example:
///
/// ```swift
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
/// let snapshot = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshotPool in
/// try db.inTransaction {
/// try Player.deleteAll()
/// return .commit
/// }
///
/// // Create the snapshot after all players have been deleted.
/// return DatabaseSnapshotPool(db)
/// }
///
/// // Later... Maybe some players have been created.
/// // The snapshot is guaranteed to see an empty table of players, though:
/// let count = try snapshot.read { db in
/// try Player.fetchCount(db)
/// }
/// assert(count == 0)
/// ```
///
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
/// or if this method is called from a write transaction, or if the
/// wal file is missing or truncated (size zero).
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
///
/// - parameter db: A database connection.
/// - parameter configuration: A configuration. If nil, the configuration of
/// `db` is used.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public init(_ db: Database, configuration: Configuration? = nil) throws {
var configuration = Self.configure(configuration ?? db.configuration)
// Acquire and hold WAL snapshot
let walSnapshot = try db.isolated(readOnly: true) {
try WALSnapshot(db)
}
var holderConfig = Configuration()
holderConfig.allowsUnsafeTransactions = true
snapshotHolder = try DatabaseQueue(path: db.path, configuration: holderConfig)
try snapshotHolder.inDatabase { db in
try db.beginTransaction(.deferred)
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
let code = sqlite3_snapshot_open(db.sqliteConnection, "main", walSnapshot.sqliteSnapshot)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code)
}
}
configuration.prepareDatabase { db in
try db.beginTransaction(.deferred)
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
let code = sqlite3_snapshot_open(db.sqliteConnection, "main", walSnapshot.sqliteSnapshot)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code)
}
}
self.configuration = configuration
self.path = db.path
self.walSnapshot = walSnapshot
var readerCount = 0
readerPool = Pool(
maximumCount: configuration.maximumReaderCount,
qos: configuration.readQoS,
makeElement: {
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
return try SerializedDatabase(
path: db.path,
configuration: configuration,
defaultLabel: "GRDB.DatabaseSnapshotPool",
purpose: "snapshot.\(readerCount)")
})
}
/// Creates a snapshot of the database.
///
/// For example:
///
/// ```swift
/// let snapshot = try DatabaseSnapshotPool(path: "/path/to/database.sqlite")
/// ```
///
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
/// or if the wal file is missing or truncated (size zero).
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
///
/// - parameters:
/// - path: The path to the database file.
/// - configuration: A configuration.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public init(path: String, configuration: Configuration = Configuration()) throws {
var configuration = Self.configure(configuration)
// Acquire and hold WAL snapshot
var holderConfig = Configuration()
holderConfig.allowsUnsafeTransactions = true
snapshotHolder = try DatabaseQueue(path: path, configuration: holderConfig)
let walSnapshot = try snapshotHolder.inDatabase { db in
try db.beginTransaction(.deferred)
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
return try WALSnapshot(db)
}
configuration.prepareDatabase { db in
try db.beginTransaction(.deferred)
try db.execute(sql: "SELECT rootpage FROM sqlite_master LIMIT 1")
let code = sqlite3_snapshot_open(db.sqliteConnection, "main", walSnapshot.sqliteSnapshot)
guard code == SQLITE_OK else {
throw DatabaseError(resultCode: code)
}
}
self.configuration = configuration
self.path = path
self.walSnapshot = walSnapshot
var readerCount = 0
readerPool = Pool(
maximumCount: configuration.maximumReaderCount,
qos: configuration.readQoS,
makeElement: {
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
return try SerializedDatabase(
path: path,
configuration: configuration,
defaultLabel: "GRDB.DatabaseSnapshotPool",
purpose: "snapshot.\(readerCount)")
})
}
private static func configure(_ configuration: Configuration) -> Configuration {
var configuration = configuration
// DatabaseSnapshotPool needs a non-empty pool of connections.
GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1")
// DatabaseSnapshotPool is read-only.
configuration.readonly = true
// DatabaseSnapshotPool uses deferred transactions by default.
// Other transaction kinds are forbidden by SQLite in read-only connections.
configuration.defaultTransactionKind = .deferred
// DatabaseSnapshotPool keeps a long-lived transaction.
configuration.allowsUnsafeTransactions = true
// DatabaseSnapshotPool requires the WAL mode.
// See <https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode>
if configuration.readonlyBusyMode == nil {
configuration.readonlyBusyMode = .timeout(10)
}
return configuration
}
}
extension DatabaseSnapshotPool: @unchecked Sendable { }
extension DatabaseSnapshotPool: DatabaseSnapshotReader {
public func close() throws {
try readerPool?.barrier {
defer { readerPool = nil }
try readerPool?.forEach { reader in
try reader.sync { try $0.close() }
}
}
}
public func interrupt() {
readerPool?.forEach { $0.interrupt() }
}
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
public func read<T>(_ value: (Database) throws -> T) throws -> T {
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
guard let readerPool else {
throw DatabaseError.connectionIsClosed()
}
let (reader, releaseReader) = try readerPool.get()
var completion: PoolCompletion!
defer {
releaseReader(completion)
}
return try reader.sync { db in
do {
let value = try value(db)
completion = poolCompletion(db)
return value
} catch {
completion = poolCompletion(db)
throw error
}
}
}
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
guard let readerPool else {
value(.failure(DatabaseError.connectionIsClosed()))
return
}
readerPool.asyncGet { result in
do {
let (reader, releaseReader) = try result.get()
// Second async jump because that's how `Pool.async` has to be used.
reader.async { db in
value(.success(db))
releaseReader(self.poolCompletion(db))
}
} catch {
value(.failure(error))
}
}
}
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
if let reader = currentReader {
return try reader.reentrantSync { db in
let result = try value(db)
if snapshotIsLost(db) {
throw DatabaseError(resultCode: .SQLITE_ABORT, message: "Snapshot is lost.")
}
return result
}
} else {
// There is no unsafe access to a snapshot.
return try read(value)
}
}
public func _add<Reducer>(
observation: ValueObservation<Reducer>,
scheduling scheduler: some ValueObservationScheduler,
onChange: @escaping (Reducer.Value) -> Void)
-> AnyDatabaseCancellable where Reducer: ValueReducer
{
_addReadOnly(observation: observation, scheduling: scheduler, onChange: onChange)
}
/// Returns a reader that can be used from the current dispatch queue,
/// if any.
private var currentReader: SerializedDatabase? {
guard let readerPool else {
return nil
}
var readers: [SerializedDatabase] = []
readerPool.forEach { reader in
// We can't check for reader.onValidQueue here because
// Pool.forEach() runs its closure argument in some arbitrary
// dispatch queue. We thus extract the reader so that we can query
// it below.
readers.append(reader)
}
// Now the readers array contains some readers. The pool readers may
// already be different, because some other thread may have started
// a new read, for example.
//
// This doesn't matter: the reader we are looking for is already on
// its own dispatch queue. If it exists, is still in use, thus still
// in the pool, and thus still relevant for our check:
return readers.first { $0.onValidQueue }
}
private func poolCompletion(_ db: Database) -> PoolCompletion {
snapshotIsLost(db) ? .discard : .reuse
}
private func snapshotIsLost(_ db: Database) -> Bool {
do {
let currentSnapshot = try WALSnapshot(db)
if currentSnapshot.compare(walSnapshot) == 0 {
return false
} else {
return true
}
} catch {
return true
}
}
}
#endif
@@ -0,0 +1,386 @@
import Foundation
/// A value stored in a database table.
///
/// To get `DatabaseValue` instances, you can:
///
/// - Fetch `DatabaseValue` from a ``Database`` instace:
///
/// ```swift
/// try dbQueue.read { db in
/// let dbValue = try DatabaseValue.fetchOne(db, sql: """
/// SELECT name FROM player
/// """)
/// }
/// ```
///
/// - Extract `DatabaseValue` from a database ``Row``:
///
/// ```swift
/// try dbQueue.read { db in
/// if let row = try Row.fetchOne(db, sql: """
/// SELECT name FROM player
/// """)
/// {
/// let dbValue = row[0] as DatabaseValue
/// }
/// }
/// ```
///
/// - Use the ``DatabaseValueConvertible/databaseValue-1ob9k`` property on a
/// ``DatabaseValueConvertible`` value:
///
/// ```swift
/// let dbValue = DatabaseValue.null
/// let dbValue = 1.databaseValue
/// let dbValue = "Arthur".databaseValue
/// let dbValue = Date().databaseValue
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html>
///
/// ## Topics
///
/// ### Creating a DatabaseValue
///
/// - ``init(value:)``
/// - ``init(sqliteStatement:index:)``
/// - ``null``
///
/// ### Accessing the SQLite storage
///
/// - ``isNull``
/// - ``storage-swift.property``
/// - ``Storage-swift.enum``
public struct DatabaseValue: Hashable {
/// The SQLite storage.
public let storage: Storage
/// The NULL DatabaseValue.
public static let null = DatabaseValue(storage: .null)
/// A value stored in a database table, with its exact SQLite storage
/// (NULL, INTEGER, REAL, TEXT, BLOB).
///
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes>
@frozen
public enum Storage {
/// The NULL storage class.
case null
/// The INTEGER storage class, wrapping an Int64.
case int64(Int64)
/// The REAL storage class, wrapping a Double.
case double(Double)
/// The TEXT storage class, wrapping a String.
case string(String)
/// The BLOB storage class, wrapping Data.
case blob(Data)
/// Returns `Int64`, `Double`, `String`, `Data` or nil.
public var value: (any DatabaseValueConvertible)? {
switch self {
case .null:
return nil
case .int64(let int64):
return int64
case .double(let double):
return double
case .string(let string):
return string
case .blob(let data):
return data
}
}
}
/// Creates a `DatabaseValue` from any value.
///
/// The result is nil unless `value` adopts ``DatabaseValueConvertible``.
public init?(value: Any) {
guard let convertible = value as? any DatabaseValueConvertible else {
return nil
}
self = convertible.databaseValue
}
// MARK: - Extracting Value
/// A boolean value indicating is the database value is `NULL`.
public var isNull: Bool {
switch storage {
case .null:
return true
default:
return false
}
}
// MARK: - Not Public
init(storage: Storage) {
self.storage = storage
}
// SQLite function argument
init(sqliteValue: SQLiteValue) {
switch sqlite3_value_type(sqliteValue) {
case SQLITE_NULL:
storage = .null
case SQLITE_INTEGER:
storage = .int64(sqlite3_value_int64(sqliteValue))
case SQLITE_FLOAT:
storage = .double(sqlite3_value_double(sqliteValue))
case SQLITE_TEXT:
storage = .string(String(cString: sqlite3_value_text(sqliteValue)!))
case SQLITE_BLOB:
if let bytes = sqlite3_value_blob(sqliteValue) {
let count = Int(sqlite3_value_bytes(sqliteValue))
storage = .blob(Data(bytes: bytes, count: count)) // copy bytes
} else {
storage = .blob(Data())
}
case let type:
// Assume a GRDB bug: there is no point throwing any error.
fatalError("Unexpected SQLite value type: \(type)")
}
}
/// Creates a `DatabaseValue` initialized from a raw SQLite statement pointer.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_NULL:
storage = .null
case SQLITE_INTEGER:
storage = .int64(sqlite3_column_int64(sqliteStatement, index))
case SQLITE_FLOAT:
storage = .double(sqlite3_column_double(sqliteStatement, index))
case SQLITE_TEXT:
storage = .string(String(cString: sqlite3_column_text(sqliteStatement, index)))
case SQLITE_BLOB:
if let bytes = sqlite3_column_blob(sqliteStatement, index) {
let count = Int(sqlite3_column_bytes(sqliteStatement, index))
storage = .blob(Data(bytes: bytes, count: count)) // copy bytes
} else {
storage = .blob(Data())
}
case let type:
// Assume a GRDB bug: there is no point throwing any error.
fatalError("Unexpected SQLite column type: \(type)")
}
}
}
extension DatabaseValue: StatementBinding {
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
switch storage {
case .null:
return sqlite3_bind_null(sqliteStatement, index)
case .int64(let int64):
return int64.bind(to: sqliteStatement, at: index)
case .double(let double):
return double.bind(to: sqliteStatement, at: index)
case .string(let string):
return string.bind(to: sqliteStatement, at: index)
case .blob(let data):
return data.bind(to: sqliteStatement, at: index)
}
}
/// Calls the given closure after binding a statement argument.
///
/// The binding is valid only during the execution of this method.
///
/// - parameter sqliteStatement: An SQLite statement.
/// - parameter index: 1-based index to statement arguments.
/// - parameter body: The closure to execute when argument is bound.
func withBinding<T>(to sqliteStatement: SQLiteStatement, at index: CInt, do body: () throws -> T) throws -> T {
switch storage {
case .null:
let code = sqlite3_bind_null(sqliteStatement, index)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
case .int64(let int64):
let code = int64.bind(to: sqliteStatement, at: index)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
case .double(let double):
let code = double.bind(to: sqliteStatement, at: index)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
case .string(let string):
return try string.withBinding(to: sqliteStatement, at: index, do: body)
case .blob(let data):
return try data.withBinding(to: sqliteStatement, at: index, do: body)
}
}
}
extension DatabaseValue: Sendable { }
// @unchecked Sendable because Data is not Sendable in all target OS
extension DatabaseValue.Storage: @unchecked Sendable { }
// MARK: - Hashable & Equatable
extension DatabaseValue.Storage: Equatable {
/// Return true if the storages are identical.
///
/// Unlike ``DatabaseValue`` equality that considers the integer 1 as
/// equal to the 1.0 double (as SQLite does), int64 and double storages
/// are never equal.
public static func == (_ lhs: Self, _ rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.null, .null): return true
case let (.int64(lhs), .int64(rhs)): return lhs == rhs
case let (.double(lhs), .double(rhs)): return lhs == rhs
case let (.string(lhs), .string(rhs)): return lhs == rhs
case let (.blob(lhs), .blob(rhs)): return lhs == rhs
default: return false
}
}
}
extension DatabaseValue: Equatable {
/// Returns whether two ``DatabaseValue`` are equal.
///
/// For example:
///
/// ```swift
/// 1.databaseValue == "foo".databaseValue // false
/// 1.databaseValue == 1.databaseValue // true
/// ```
///
/// When comparing integers and doubles, the result is true if and only
/// values are equal, and if converting one type to the other does
/// not lose information:
///
/// ```swift
/// 1.databaseValue == 1.0.databaseValue // true
/// ```
///
/// For a comparison that distinguishes integer and doubles, compare
/// storages instead:
///
/// ```swift
/// 1.databaseValue.storage == 1.0.databaseValue.storage // false
/// ```
public static func == (lhs: DatabaseValue, rhs: DatabaseValue) -> Bool {
switch (lhs.storage, rhs.storage) {
case (.null, .null):
return true
case let (.int64(lhs), .int64(rhs)):
return lhs == rhs
case let (.double(lhs), .double(rhs)):
return lhs == rhs
case let (.int64(lhs), .double(rhs)):
return Int64(exactly: rhs) == lhs
case let (.double(lhs), .int64(rhs)):
return rhs == Int64(exactly: lhs)
case let (.string(lhs), .string(rhs)):
return lhs == rhs
case let (.blob(lhs), .blob(rhs)):
return lhs == rhs
default:
return false
}
}
}
extension DatabaseValue {
public func hash(into hasher: inout Hasher) {
switch storage {
case .null:
hasher.combine(0)
case .int64(let int64):
// 1 == 1.0, hence 1 and 1.0 must have the same hash:
hasher.combine(Double(int64))
case .double(let double):
hasher.combine(double)
case .string(let string):
hasher.combine(string)
case .blob(let data):
hasher.combine(data)
}
}
}
extension DatabaseValue: DatabaseValueConvertible {
/// Returns self
public var databaseValue: DatabaseValue {
self
}
/// Returns the database value
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> DatabaseValue? {
dbValue
}
}
extension DatabaseValue: SQLSpecificExpressible {
public var sqlExpression: SQLExpression {
.databaseValue(self)
}
}
extension DatabaseValue: CustomStringConvertible {
public var description: String {
switch storage {
case .null:
return "NULL"
case .int64(let int64):
return String(int64)
case .double(let double):
return String(double)
case .string(let string):
return String(reflecting: string)
case .blob(let data):
return "Data(\(data.description))"
}
}
}
/// Compares DatabaseValue like SQLite.
///
/// See RxGRDB for tests.
///
/// This comparison is not public because it does not handle text collations,
/// and may be dangerous when put in user hands.
///
/// So far, the only goal of this sorting method so far is aesthetic, and
/// easier testing.
func < (lhs: DatabaseValue, rhs: DatabaseValue) -> Bool {
switch (lhs.storage, rhs.storage) {
case let (.int64(lhs), .int64(rhs)):
return lhs < rhs
case let (.double(lhs), .double(rhs)):
return lhs < rhs
case let (.int64(lhs), .double(rhs)):
return Double(lhs) < rhs
case let (.double(lhs), .int64(rhs)):
return lhs < Double(rhs)
case let (.string(lhs), .string(rhs)):
return lhs.utf8.lexicographicallyPrecedes(rhs.utf8)
case let (.blob(lhs), .blob(rhs)):
return lhs.lexicographicallyPrecedes(rhs, by: <)
case (.blob, _):
return false
case (_, .blob):
return true
case (.string, _):
return false
case (_, .string):
return true
case (.int64, _), (.double, _):
return false
case (_, .int64), (_, .double):
return true
case (.null, _):
return false
case (_, .null):
return true
}
}
@@ -0,0 +1,831 @@
import Foundation
// Standard collections `Array`, `Set`, and `Dictionary` do not conform to
// `DatabaseValueConvertible`, on purpose.
//
// Adding `DatabaseValueConvertible` conformance to those collection types
// would litter JSON values in unexpected places, and foster misuse. For
// example, it is better when the code below *does not compile*:
//
// ```swift
// // MISUSE: if Array would conform to DatabaseValueConvertible, this
// // code would compile, and run the incorrect SQLite query
// // `SELECT ... WHERE id IN ('[1,2,3]')`, instead of the expected
// // `SELECT ... WHERE id IN (1, 2, 3)`.
// let ids = [1, 2, 3]
// let players = try Player.fetchAll(db, sql: """
// SELECT * FROM player WHERE id IN (?)
// """, arguments: [ids])
// ```
//
// Correct and fostered versions of the code above are:
//
// ```swift
// // CORRECT (explicit SQLite arguments):
// let ids = [1, 2, 3]
// let questionMarks = databaseQuestionMarks(count: ids.count) // "?,?,?"
// let players = try Player.fetchAll(db, sql: """
// SELECT * FROM player WHERE id IN (\(questionMarks))
// """, arguments: StatementArguments(ids))
//
// // CORRECT (SQL interpolation):
// let ids = [1, 2, 3]
// let request: SQLRequest<Player> = """
// SELECT * FROM player WHERE id IN \(ids)
// """
// let players = try request.fetchAll(db)
// ```
public protocol DatabaseValueConvertible: SQLExpressible, StatementBinding {
/// A database value.
var databaseValue: DatabaseValue { get }
/// Creates an instance with the specified database value.
///
/// If there is no value of the type that corresponds with the specified
/// database value, this method returns nil. For example:
///
/// ```swift
/// let dbValue = "Arthur".databaseValue
///
/// String.fromDatabaseValue(dbValue) // "Arthur"
/// Int.fromDatabaseValue(dbValue) // nil
/// ```
///
/// - parameter dbValue: A DatabaseValue.
/// - returns: A decoded value, or, if decoding is impossible, nil.
static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self?
/// Creates an instance from a missing column, if possible.
///
/// - warning: Do not customize the default implementation.
///
/// - returns: A decoded value, or, if decoding is impossible, nil.
static func fromMissingColumn() -> Self?
/// Returns the `JSONDecoder` that decodes the value.
///
/// This method is dedicated to ``DatabaseValueConvertible`` types that
/// also conform to the standard `Decodable` protocol.
static func databaseJSONDecoder() -> JSONDecoder
/// Returns the `JSONEncoder` that encodes the value.
///
/// This method is dedicated to ``DatabaseValueConvertible`` types that
/// also conform to the standard `Encodable` protocol.
static func databaseJSONEncoder() -> JSONEncoder
}
extension DatabaseValueConvertible {
public var sqlExpression: SQLExpression {
.databaseValue(databaseValue)
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
databaseValue.bind(to: sqliteStatement, at: index)
}
// `Optional` overrides this default behavior.
/// Default implementation fails to decode a value from a missing column.
public static func fromMissingColumn() -> Self? {
nil // failure.
}
/// Returns the `JSONDecoder` that decodes the value.
///
/// The default implementation returns a `JSONDecoder` with the
/// following properties:
///
/// - `dataDecodingStrategy`: `.base64`
/// - `dateDecodingStrategy`: `.millisecondsSince1970`
/// - `nonConformingFloatDecodingStrategy`: `.throw`
public static func databaseJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
decoder.dateDecodingStrategy = .millisecondsSince1970
decoder.nonConformingFloatDecodingStrategy = .throw
return decoder
}
/// Returns the `JSONEncoder` that encodes the value.
///
/// The default implementation returns a `JSONEncoder` with the
/// following properties:
///
/// - `dataEncodingStrategy`: `.base64`
/// - `dateEncodingStrategy`: `.millisecondsSince1970`
/// - `nonConformingFloatEncodingStrategy`: `.throw`
/// - `outputFormatting`: `.sortedKeys`
public static func databaseJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
encoder.dataEncodingStrategy = .base64
encoder.dateEncodingStrategy = .millisecondsSince1970
encoder.nonConformingFloatEncodingStrategy = .throw
// guarantee some stability in order to ease value comparison
encoder.outputFormatting = .sortedKeys
return encoder
}
}
// MARK: - Conversions
extension DatabaseValueConvertible {
static func decode(
fromDatabaseValue dbValue: DatabaseValue,
context: @autoclosure () -> RowDecodingContext)
throws -> Self
{
if let value = fromDatabaseValue(dbValue) {
return value
} else {
throw RowDecodingError.valueMismatch(Self.self, context: context(), databaseValue: dbValue)
}
}
static func decode(
fromStatement sqliteStatement: SQLiteStatement,
atUncheckedIndex index: CInt,
context: @autoclosure () -> RowDecodingContext)
throws -> Self
{
let dbValue = DatabaseValue(sqliteStatement: sqliteStatement, index: index)
return try decode(fromDatabaseValue: dbValue, context: context())
}
@usableFromInline
static func decode(fromRow row: Row, atUncheckedIndex index: Int) throws -> Self {
if let sqliteStatement = row.sqliteStatement {
return try decode(
fromStatement: sqliteStatement,
atUncheckedIndex: CInt(index),
context: RowDecodingContext(row: row, key: .columnIndex(index)))
}
return try decode(
fromDatabaseValue: row.impl.databaseValue(atUncheckedIndex: index),
context: RowDecodingContext(row: row, key: .columnIndex(index)))
}
@usableFromInline
static func decodeIfPresent(fromRow row: Row, atUncheckedIndex index: Int) throws -> Self? {
try Optional<Self>.decode(fromRow: row, atUncheckedIndex: index)
}
}
// MARK: - Cursors
/// A cursor of database values.
///
/// A `DatabaseValueCursor` iterates all rows from a database request. Its
/// elements are the database values decoded from the leftmost column.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let names: DatabaseValueCursor<String> = try String.fetchCursor(db, sql: """
/// SELECT name FROM player
/// """)
/// while let name = names.next() { // String
/// print(name)
/// }
/// }
/// ```
public final class DatabaseValueCursor<Value: DatabaseValueConvertible>: DatabaseCursor {
public typealias Element = Value
public let _statement: Statement
public var _isDone = false
private let columnIndex: CInt
init(statement: Statement, arguments: StatementArguments? = nil, adapter: (any RowAdapter)? = nil) throws {
self._statement = statement
if let adapter {
// adapter may redefine the index of the leftmost column
columnIndex = try CInt(adapter.baseColumnIndex(atIndex: 0, layout: statement))
} else {
columnIndex = 0
}
// Assume cursor is created for immediate iteration: reset and set arguments
try statement.prepareExecution(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? _statement.reset()
}
public func _element(sqliteStatement: SQLiteStatement) throws -> Value {
try Value.decode(
fromStatement: sqliteStatement,
atUncheckedIndex: columnIndex,
context: RowDecodingContext(statement: _statement, index: Int(columnIndex)))
}
}
// Explicit non-conformance to Sendable: database cursors must be used from
// a serialized database access dispatch queue.
@available(*, unavailable)
extension DatabaseValueCursor: Sendable { }
/// DatabaseValueConvertible comes with built-in methods that allow to fetch
/// cursors, arrays, or single values:
///
/// try String.fetchCursor(db, sql: "SELECT name FROM ...", arguments:...) // Cursor of String
/// try String.fetchAll(db, sql: "SELECT name FROM ...", arguments:...) // [String]
/// try String.fetchOne(db, sql: "SELECT name FROM ...", arguments:...) // String?
///
/// let statement = try db.makeStatement(sql: "SELECT name FROM ...")
/// try String.fetchCursor(statement, arguments:...) // Cursor of String
/// try String.fetchAll(statement, arguments:...) // [String]
/// try String.fetchOne(statement, arguments:...) // String
///
/// DatabaseValueConvertible is adopted by Bool, Int, String, etc.
extension DatabaseValueConvertible {
// MARK: Fetching From Prepared Statement
/// Returns a cursor over values fetched from a prepared statement.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let statement = try db.makeStatement(sql: sql)
/// let scores = try Int.fetchCursor(statement, arguments: [lastName])
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A ``DatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchCursor(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> DatabaseValueCursor<Self>
{
try DatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of values fetched from a prepared statement.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let statement = try db.makeStatement(sql: sql)
/// let scores = try Int.fetchAll(statement, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchAll(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> [Self]
{
try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from a prepared statement.
///
/// The value is decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ? LIMIT 1"
/// let statement = try db.makeStatement(sql: sql)
/// let score = try Int.fetchOne(statement, arguments: [lastName])
/// }
/// ```
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchOne(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> Self?
{
// fetchOne returns nil if there is no row, or if there is a row with a null value
let cursor = try DatabaseValueCursor<Self?>(statement: statement, arguments: arguments, adapter: adapter)
return try cursor.next() ?? nil
}
}
extension DatabaseValueConvertible where Self: Hashable {
/// Returns a set of values fetched from a prepared statement.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let statement = try db.makeStatement(sql: sql)
/// let scores = try Int.fetchSet(statement, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A set.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchSet(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> Set<Self>
{
try Set(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
}
extension DatabaseValueConvertible {
// MARK: Fetching From SQL
/// Returns a cursor over values fetched from an SQL query.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let scores = try Int.fetchCursor(db, sql: sql, arguments: [lastName])
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A ``DatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchCursor(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> DatabaseValueCursor<Self>
{
try fetchCursor(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of values fetched from an SQL query.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let scores = try Int.fetchAll(db, sql: sql, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchAll(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> [Self]
{
try fetchAll(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from an SQL query.
///
/// The value is decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let score = try Int.fetchOne(db, sql: sql, arguments: [lastName])
/// }
/// ```
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchOne(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> Self?
{
try fetchOne(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
}
extension DatabaseValueConvertible where Self: Hashable {
/// Returns a set of values fetched from an SQL query.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let scores = try Int.fetchSet(db, sql: sql, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A set.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchSet(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> Set<Self>
{
try fetchSet(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
}
extension DatabaseValueConvertible {
// MARK: Fetching From FetchRequest
/// Returns a cursor over values fetched from a fetch request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try Int.fetchCursor(db, request)
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: A ``DatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ request: some FetchRequest) throws -> DatabaseValueCursor<Self> {
let request = try request.makePreparedRequest(db, forSingleResult: false)
return try fetchCursor(request.statement, adapter: request.adapter)
}
/// Returns an array of values fetched from a fetch request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try Int.fetchAll(db, request)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: An array.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] {
let request = try request.makePreparedRequest(db, forSingleResult: false)
return try fetchAll(request.statement, adapter: request.adapter)
}
/// Returns a single value fetched from a fetch request.
///
/// The value is decoded from the leftmost column.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName) LIMIT 1
/// """
///
/// let scores = try Int.fetchOne(db, request)
/// }
/// ```
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? {
let request = try request.makePreparedRequest(db, forSingleResult: true)
return try fetchOne(request.statement, adapter: request.adapter)
}
}
extension DatabaseValueConvertible where Self: Hashable {
/// Returns a set of values fetched from a fetch request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try Int.fetchAll(db, request)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: A set.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set<Self> {
let request = try request.makePreparedRequest(db, forSingleResult: false)
return try fetchSet(request.statement, adapter: request.adapter)
}
}
extension FetchRequest where RowDecoder: DatabaseValueConvertible {
// MARK: Fetching Values
/// Returns a cursor over fetched values.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try request.fetchCursor(db)
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameter db: A database connection.
/// - returns: A ``DatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchCursor(_ db: Database) throws -> DatabaseValueCursor<RowDecoder> {
try RowDecoder.fetchCursor(db, self)
}
/// Returns an array of fetched values.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try request.fetchAll(db)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameter db: A database connection.
/// - returns: An array of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
try RowDecoder.fetchAll(db, self)
}
/// Returns a single fetched value.
///
/// The value is decoded from the leftmost column.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName) LIMIT 1
/// """
///
/// let score = try request.fetchOne(db)
/// }
/// ```
///
/// - parameter db: A database connection.
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchOne(_ db: Database) throws -> RowDecoder? {
try RowDecoder.fetchOne(db, self)
}
}
extension FetchRequest where RowDecoder: DatabaseValueConvertible & Hashable {
/// Returns a set of fetched values.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try request.fetchSet(db)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameter db: A database connection.
/// - returns: A set of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchSet(_ db: Database) throws -> Set<RowDecoder> {
try RowDecoder.fetchSet(db, self)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,311 @@
// MARK: - FetchRequest
/// A type that fetches and decodes database rows.
///
/// The main kinds of fetch requests are ``SQLRequest``
/// and ``QueryInterfaceRequest``:
///
/// ```swift
/// let lastName = "O'Reilly"
///
/// // SQLRequest
/// let request: SQLRequest<Player> = """
/// SELECT * FROM player WHERE lastName = \(lastName)
/// """
///
/// // QueryInterfaceRequest
/// let request = Player.filter(Column("lastName") == lastName)
///
/// // Use the request
/// try dbQueue.read { db in
/// let players = try request.fetchAll(db) // [Player]
/// }
/// ```
///
/// ## Topics
///
/// ### Counting the Results
///
/// - ``fetchCount(_:)``
///
/// ### Fetching Database Rows
///
/// - ``fetchCursor(_:)-9283d``
/// - ``fetchAll(_:)-7p809``
/// - ``fetchOne(_:)-9fafl``
/// - ``fetchSet(_:)-6bdrd``
///
/// ### Fetching Database Values
///
/// - ``fetchCursor(_:)-19f5g``
/// - ``fetchCursor(_:)-66xoi``
/// - ``fetchAll(_:)-1loau``
/// - ``fetchAll(_:)-28pne``
/// - ``fetchOne(_:)-44mvv``
/// - ``fetchOne(_:)-5hlkf``
/// - ``fetchSet(_:)-4hhtm``
/// - ``fetchSet(_:)-9wshm``
///
/// ### Fetching Records
///
/// - ``fetchCursor(_:)-2ah3q``
/// - ``fetchAll(_:)-vdos``
/// - ``fetchOne(_:)-2bq0k``
/// - ``fetchSet(_:)-4jdrq``
///
/// ### Preparing Database Requests
///
/// - ``makePreparedRequest(_:forSingleResult:)``
/// - ``PreparedRequest``
///
/// ### Adapting the Fetched Rows
///
/// - ``adapted(_:)``
/// - ``AdaptedFetchRequest``
///
/// ### Supporting Types
///
/// - ``AnyFetchRequest``
public protocol FetchRequest<RowDecoder>: SQLSubqueryable, DatabaseRegionConvertible {
/// The type that tells how fetched database rows should be interpreted.
associatedtype RowDecoder
/// Returns a ``PreparedRequest``.
///
/// The `singleResult` argument is a hint that a single result row will be
/// consumed. Implementations can optionally use it to optimize the
/// prepared statement, for example by adding a `LIMIT 1` SQL clause:
///
/// ```swift
/// // Calls makePreparedRequest(db, forSingleResult: true)
/// try request.fetchOne(db)
///
/// // Calls makePreparedRequest(db, forSingleResult: false)
/// try request.fetchAll(db)
/// ```
///
/// - parameter db: A database connection.
/// - parameter singleResult: A hint that a single result row will be
/// consumed.
func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest
/// Returns the number of rows fetched by the request.
///
/// - parameter db: A database connection.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
func fetchCount(_ db: Database) throws -> Int
}
extension FetchRequest {
/// Returns the database region that the request feeds from.
///
/// - parameter db: A database connection.
public func databaseRegion(_ db: Database) throws -> DatabaseRegion {
try makePreparedRequest(db, forSingleResult: false).statement.databaseRegion
}
}
// MARK: - PreparedRequest
/// A closure executed before a supplementary fetch is performed.
///
/// Support for `Database.dumpRequest`.
///
/// - parameter request: The supplementary request
/// - parameter keyPath: The key path target of the supplementary fetch.
typealias WillExecuteSupplementaryRequest = (_ request: AnyFetchRequest<Row>, _ keyPath: [String]) throws -> Void
/// A closure that performs supplementary fetches.
///
/// Support for eager loading of hasMany associations.
///
/// - parameter db: A database connection.
/// - parameter rows: The rows that are modified by the supplementary fetch.
/// - parameter willExecuteSupplementaryRequest: A closure to execute before
/// performing supplementary fetches.
typealias SupplementaryFetch = (
_ db: Database,
_ rows: [Row],
_ willExecuteSupplementaryRequest: WillExecuteSupplementaryRequest?)
throws -> Void
/// A `PreparedRequest` is a request that is ready to be executed.
public struct PreparedRequest {
/// A prepared statement with bound parameters.
public var statement: Statement
/// An eventual adapter for rows fetched by the select statement.
public var adapter: (any RowAdapter)?
/// A closure that performs supplementary fetches.
/// Support for eager loading of hasMany associations.
var supplementaryFetch: SupplementaryFetch?
init(
statement: Statement,
adapter: (any RowAdapter)?,
supplementaryFetch: SupplementaryFetch? = nil)
{
self.statement = statement
self.adapter = adapter
self.supplementaryFetch = supplementaryFetch
}
}
// Explicit non-conformance to Sendable: `PreparedRequest` contains
// a statement.
@available(*, unavailable)
extension PreparedRequest: Sendable { }
extension PreparedRequest: Refinable { }
// MARK: - AdaptedFetchRequest
extension FetchRequest {
/// Returns an adapted request.
///
/// The returned request performs an identical database query, but adapts
/// the fetched rows. See ``RowAdapter``, and
/// ``splittingRowAdapters(columnCounts:)`` for a sample code that uses
/// `adapted(_:)`.
///
/// - parameter adapter: A closure that accepts a database connection and
/// returns a row adapter.
public func adapted(_ adapter: @escaping (Database) throws -> any RowAdapter) -> AdaptedFetchRequest<Self> {
AdaptedFetchRequest(self, adapter)
}
}
/// An adapted request.
///
/// See ``FetchRequest/adapted(_:)``.
public struct AdaptedFetchRequest<Base: FetchRequest> {
let base: Base
let adapter: (Database) throws -> any RowAdapter
/// Creates an adapted request from a base request and a closure that builds
/// a row adapter from a database connection.
init(_ base: Base, _ adapter: @escaping (Database) throws -> any RowAdapter) {
self.base = base
self.adapter = adapter
}
}
extension AdaptedFetchRequest: SQLSubqueryable {
public var sqlSubquery: SQLSubquery {
base.sqlSubquery
}
}
extension AdaptedFetchRequest: FetchRequest {
public typealias RowDecoder = Base.RowDecoder
public func fetchCount(_ db: Database) throws -> Int {
try base.fetchCount(db)
}
public func makePreparedRequest(
_ db: Database,
forSingleResult singleResult: Bool = false)
throws -> PreparedRequest
{
var preparedRequest = try base.makePreparedRequest(db, forSingleResult: singleResult)
if let baseAdapter = preparedRequest.adapter {
preparedRequest.adapter = try ChainedAdapter(first: baseAdapter, second: adapter(db))
} else {
preparedRequest.adapter = try adapter(db)
}
return preparedRequest
}
}
// MARK: - AnyFetchRequest
/// A type-erased FetchRequest.
///
/// An `AnyFetchRequest` forwards its operations to an underlying request,
/// hiding its specifics.
public struct AnyFetchRequest<RowDecoder> {
private let request: FetchRequestEraser
/// Returns a request that performs an identical database query, but decodes
/// database rows with `type`.
///
/// For example:
///
/// ```swift
/// // AnyFetchRequest<Player>
/// let playerRequest = AnyFetchRequest(Player.all())
///
/// // AnyFetchRequest<Row>
/// let rowRequest = playerRequest.asRequest(of: Row.self)
public func asRequest<T>(of type: T.Type) -> AnyFetchRequest<T> {
AnyFetchRequest<T>(request: request)
}
}
extension AnyFetchRequest {
/// Creates a request that wraps and forwards operations to `request`.
public init(_ request: some FetchRequest<RowDecoder>) {
self.init(request: ConcreteFetchRequestEraser(request: request))
}
}
extension AnyFetchRequest: SQLSubqueryable {
public var sqlSubquery: SQLSubquery {
request.sqlSubquery
}
}
extension AnyFetchRequest: FetchRequest {
public func fetchCount(_ db: Database) throws -> Int {
try request.fetchCount(db)
}
public func makePreparedRequest(
_ db: Database,
forSingleResult singleResult: Bool = false)
throws -> PreparedRequest
{
try request.makePreparedRequest(db, forSingleResult: singleResult)
}
}
// Class-based type erasure, so that we preserve full type information.
private class FetchRequestEraser: FetchRequest {
typealias RowDecoder = Void
var sqlSubquery: SQLSubquery {
fatalError("subclass must override")
}
func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest {
fatalError("subclass must override")
}
func fetchCount(_ db: Database) throws -> Int {
fatalError("subclass must override")
}
}
private final class ConcreteFetchRequestEraser<Request: FetchRequest>: FetchRequestEraser {
let request: Request
init(request: Request) {
self.request = request
}
override var sqlSubquery: SQLSubquery {
request.sqlSubquery
}
override func fetchCount(_ db: Database) throws -> Int {
try request.fetchCount(db)
}
override func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest {
try request.makePreparedRequest(db, forSingleResult: singleResult)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,751 @@
import Foundation
/// Returns an array of row adapters that split a row according to the
/// provided numbers of columns.
///
/// This method is useful for splitting a row into chunks.
///
/// For example, let's consider the following SQL query:
///
/// ```swift
/// let sql = """
/// SELECT player.*, team.*
/// FROM player
/// LEFT JOIN team ON team.id = player.teamId
/// WHERE player.id = ?
/// """
/// ```
///
/// The resulting rows contains columns from both player and team tables:
///
/// ```swift
/// // [id: 1, name: "Arthur", teamId: 42, id: 42, name: "Reds"]
/// // <---------------------------------><-------------------->
/// // player columns team columns
/// let row = try Row.fetchOne(db, sql: sql, arguments: [1])
/// ```
///
/// Because some columns have the same name (`id` and `name`), it is
/// difficult to access the team columns.
///
/// `splittingRowAdapters` and ``ScopeAdapter`` make it possible to
/// access player and team columns independently, with row ``Row/scopes``:
///
/// ```swift
/// let adapters = try splittingRowAdapters([
/// db.columns(in: "player").count,
/// db.columns(in: "team").count,
/// ])
/// let adapter = ScopeAdapter([
/// "player": adapters[0],
/// "team": adapters[1],
/// ])
/// if let row = try Row.fetchOne(db, sql: sql, arguments: [1], adapter: adapter) {
/// // A Row that only contains player columns
/// // [id: 1, name: "Arthur", teamId: 42]
/// row.scopes["player"]
///
/// // A Row that only contains team columns
/// // [id: 42, name: "Reds"]
/// row.scopes["team"]
/// }
/// ```
///
/// Decoding ``FetchableRecord`` types is easy:
///
/// ```swift
/// if let row = try Row.fetchOne(db, sql: sql, arguments: [1], adapter: adapter) {
/// // Player(id: 1, name: "Arthur", teamId: 42)
/// let player: Player = row["player"]
///
/// // Team(id: 42, name: "Reds")
/// // nil if the LEFT JOIN has fetched NULL team columns
/// if let team: Team? = row["team"]
/// }
/// ```
///
/// You can package this technique in a dedicated type, as in the next
/// example. It enhances the previous sample codes with:
///
/// - Support for record types that customize their fetched columns
/// with ``TableRecord/databaseSelection-7iphs``.
/// - ``SQLRequest`` and its support for [SQL Interpolation](https://github.com/groue/GRDB.swift/blob/master/Documentation/SQLInterpolation.md).
/// - ``FetchRequest/adapted(_:)`` for building a request that embeds the
/// row adapters.
///
/// ```swift
/// struct Player: TableRecord, FetchableRecord { ... }
/// struct Team: TableRecord, FetchableRecord { ... }
///
/// struct PlayerInfo {
/// var player: Player
/// var team: Team?
/// }
///
/// extension PlayerInfo: FetchableRecord {
/// init(row: Row) {
/// player = row["player"]
/// team = row["team"]
/// }
/// }
///
/// extension PlayerInfo {
/// /// The request for the player info, given a player id
/// static func filter(playerId: Int64) -> some FetchRequest<PlayerInfo> {
/// // Build SQL request with SQL interpolation
/// let request: SQLRequest<PlayerInfo> = """
/// SELECT
/// \(columnsOf: Player.self), -- Instead of player.*
/// \(columnsOf: Team.self), -- Instead of team.*
/// FROM player
/// LEFT JOIN team ON team.id = player.teamId
/// WHERE player.id = \(playerId)
/// """
///
/// // Returns an adapted request that defines the player and team
/// // scopes in the fetched row
/// return request.adapted { db in
/// let adapters = try splittingRowAdapters(columnCounts: [
/// Player.numberOfSelectedColumns(db),
/// Team.numberOfSelectedColumns(db),
/// ])
/// return ScopeAdapter([
/// "player": adapters[0],
/// "team": adapters[1],
/// ])
/// }
/// }
/// }
///
/// // Usage
/// try dbQueue.read { db in
/// if let playerInfo = try PlayerInfo.filter(playerId: 1).fetchOne(db) {
/// print(playerInfo.player) // Player(id: 1, name: "Arthur", teamId: 42)
/// print(playerInfo.team) // Team(id: 42, name: "Reds")
/// }
/// }
/// ```
///
/// - parameter columnCounts: An array of row chunk lengths.
/// - returns: An array of row adapters that split a row into as many chunks
/// as the number of elements in `columnCounts`, plus one (the row adapter
/// for all columns that remain on the right of the last chunk).
public func splittingRowAdapters(columnCounts: [Int]) -> [any RowAdapter] {
if columnCounts.isEmpty {
// Identity adapter
return [SuffixRowAdapter(fromIndex: 0)]
}
// [1, 3, 2] -> [0, 1, 4, 6]
let columnIndexes = columnCounts.reduce(into: [0]) { (acc, count) in
acc.append(acc.last! + count)
}
// [0, 1, 4, 6] -> [(0..<1), (1..<4), (4..<6)]
let rangeAdapters = zip(columnIndexes, columnIndexes.suffix(from: 1))
.map { RangeRowAdapter($0..<$1) }
// (6...)
let suffixAdapter = SuffixRowAdapter(fromIndex: columnIndexes.last!)
// [(0..<1), (1..<4), (4..<6), (6...)]
return rangeAdapters + [suffixAdapter]
}
/// _LayoutedColumnMapping is a type that supports the RowAdapter protocol.
public struct _LayoutedColumnMapping {
/// An array of (baseIndex, mappedName) pairs, where baseIndex is the index
/// of a column in a base row, and mappedName the mapped name of
/// that column.
public let _layoutColumns: [(Int, String)]
/// A cache for layoutIndex(ofColumn:)
let lowercaseColumnIndexes: [String: Int] // [mappedColumn: layoutColumnIndex]
/// Creates a _LayoutedColumnMapping from an array of (baseIndex, mappedName)
/// pairs. In each pair:
///
/// - baseIndex is the index of a column in a base row
/// - name is the mapped name of the column
///
/// For example, the following _LayoutedColumnMapping defines two columns, "foo"
/// and "bar", based on the base columns at indexes 1 and 2:
///
/// _LayoutedColumnMapping(layoutColumns: [(1, "foo"), (2, "bar")])
///
/// Use it in your custom RowAdapter type:
///
/// struct FooBarAdapter : RowAdapter {
/// func layoutAdapter(layout: _RowLayout) throws -> any _LayoutedRowAdapter {
/// return _LayoutedColumnMapping(layoutColumns: [(1, "foo"), (2, "bar")])
/// }
/// }
///
/// // [foo:"foo" bar: "bar"]
/// try Row.fetchOne(db, sql: "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter())
init<S>(layoutColumns: S)
where S: Sequence, S.Element == (Int, String)
{
self._layoutColumns = Array(layoutColumns)
self.lowercaseColumnIndexes = Dictionary(
layoutColumns
.enumerated()
.map { ($0.element.1.lowercased(), $0.offset) },
uniquingKeysWith: { (left, _) in left }) // keep leftmost indexes
}
func baseColumnIndex(atMappingIndex index: Int) -> Int {
_layoutColumns[index].0
}
func columnName(atMappingIndex index: Int) -> String {
_layoutColumns[index].1
}
}
extension _LayoutedColumnMapping: _LayoutedRowAdapter {
/// Returns self.
public var _mapping: _LayoutedColumnMapping { self }
/// Returns the empty dictionary.
public var _scopes: [String: any _LayoutedRowAdapter] { [:] }
}
extension _LayoutedColumnMapping: _RowLayout {
/// Returns the index of the leftmost column named `name`, in a
/// case-insensitive way.
public func _layoutIndex(ofColumn name: String) -> Int? {
if let index = lowercaseColumnIndexes[name] {
return index
}
return lowercaseColumnIndexes[name.lowercased()]
}
}
/// `_LayoutedRowAdapter` is a protocol that supports the `RowAdapter` protocol.
///
/// GRBD ships with a ready-made type that adopts this protocol:
/// `_LayoutedColumnMapping`.
public protocol _LayoutedRowAdapter {
/// A LayoutedColumnMapping that defines how to map a column name to a
/// column in a base row.
var _mapping: _LayoutedColumnMapping { get }
/// The layouted row adapters for each scope.
var _scopes: [String: any _LayoutedRowAdapter] { get }
}
/// `_RowLayout` is a protocol that supports the `RowAdapter` protocol. It
/// describes the layout of a base row.
public protocol _RowLayout {
/// An array of (baseIndex, name) pairs, where baseIndex is the index
/// of a column in a base row, and name the name of that column.
var _layoutColumns: [(Int, String)] { get }
/// Returns the index of the leftmost column named `name`, in a
/// case-insensitive way.
func _layoutIndex(ofColumn name: String) -> Int?
}
extension Statement: _RowLayout {
public var _layoutColumns: [(Int, String)] {
Array(columnNames.enumerated())
}
public func _layoutIndex(ofColumn name: String) -> Int? {
index(ofColumn: name)
}
}
/// A type that helps two incompatible row interfaces working together.
///
/// Row adapters present database rows in the way expected by the
/// row consumers.
///
/// For example, when a row consumer expects a column named "consumed", but
/// the raw row has a column named "produced", the ``ColumnMapping`` row
/// adapter comes in handy:
///
/// ```swift
/// // Feeds the "consumed" column from "produced":
/// let adapter = ColumnMapping(["consumed": "produced"])
/// let sql = "SELECT 'Hello' AS produced"
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// // [consumed:"Hello"]
/// print(row)
///
/// // "Hello"
/// print(row["consumed"])
/// ```
///
/// The raw fetched columns are not lost (see ``Row/unadapted``):
///
/// ```swift
/// // [consumed:"Hello"]
/// // unadapted: [produced:"Hello"]
/// print(row.debugDescription)
///
/// // [produced:"Hello"]
/// print(row.unadapted)
/// ```
///
/// There are several situations where row adapters are useful. Among them:
///
/// - Adapters help disambiguate columns with identical names, which may
/// happen when you select columns from several tables.
/// See ``splittingRowAdapters(columnCounts:)`` for some sample code.
///
/// - Adapters help when SQLite outputs unexpected column names, which may
/// happen with some subqueries. See ``RenameColumnAdapter`` for
/// an example.
///
/// ## Topics
///
/// ### Splitting a Row into Chunks
///
/// - ``splittingRowAdapters(columnCounts:)``
///
/// ### Adding Scopes to an Adapter
///
/// - ``addingScopes(_:)``
///
/// ### Built-in Adapters
///
/// - ``ColumnMapping``
/// - ``EmptyRowAdapter``
/// - ``RangeRowAdapter``
/// - ``RenameColumnAdapter``
/// - ``ScopeAdapter``
/// - ``SuffixRowAdapter``
public protocol RowAdapter {
/// You never call this method directly. It is called for you whenever an
/// adapter has to be applied.
///
/// The result is a value that adopts _LayoutedRowAdapter, such as
/// _LayoutedColumnMapping.
///
/// For example:
///
/// // An adapter that turns any row to a row that contains a single
/// // column named "foo" whose value is the leftmost value of the
/// // base row.
/// struct FirstColumnAdapter : RowAdapter {
/// func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
/// return _LayoutedColumnMapping(layoutColumns: [(0, "foo")])
/// }
/// }
///
/// // [foo:1]
/// try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: FirstColumnAdapter())
func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter
}
extension RowAdapter {
/// Returns an adapter based on self, with added scopes.
///
/// If self already defines scopes, the added scopes replace
/// eventual existing scopes with the same name.
///
/// - parameter scopes: A dictionary that maps scope names to
/// row adapters.
public func addingScopes(_ scopes: [String: any RowAdapter]) -> any RowAdapter {
if scopes.isEmpty {
return self
} else {
return ScopeAdapter(base: self, scopes: scopes)
}
}
}
extension RowAdapter {
func baseColumnIndex(atIndex index: Int, layout: some _RowLayout) throws -> Int {
try _layoutedAdapter(from: layout)._mapping.baseColumnIndex(atMappingIndex: index)
}
}
/// `EmptyRowAdapter` is a row adapter that hides all columns.
///
/// For example:
///
/// ```swift
/// let adapter = EmptyRowAdapter()
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c"
///
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
/// row.isEmpty // true
/// ```
///
/// This limit adapter may turn out useful in some narrow use cases. You'll
/// be happy to find it when you need it.
public struct EmptyRowAdapter: RowAdapter, Sendable {
/// Creates an `EmptyRowAdapter`.
public init() { }
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
_LayoutedColumnMapping(layoutColumns: [])
}
}
/// `ColumnMapping` is a row adapter that maps column names.
///
/// Build a `ColumnMapping` with a dictionary whose keys
/// are adapted column names, and values the column names in the base row:
///
/// ```swift
/// // Feeds "newA" from "a", and "newB" from "b":
/// let adapter = ColumnMapping(["newA": "a", "newB": "b"])
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c"
///
/// // [newA:0, newB:1]
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
/// ```
///
/// Note that columns that are not present in the dictionary are not present
/// in the resulting adapted row.
public struct ColumnMapping: RowAdapter, Sendable {
/// A dictionary from mapped column names to column names in a base row.
let mapping: [String: String]
/// Creates a `ColumnMapping` with a dictionary from mapped column names
/// to column names in a base row.
public init(_ mapping: [String: String]) {
self.mapping = mapping
}
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
let layoutColumns = try mapping
.map { (mappedColumn, baseColumn) -> (Int, String) in
guard let index = layout._layoutIndex(ofColumn: baseColumn) else {
let columnNames = layout._layoutColumns.map { $0.1 }
throw DatabaseError(
resultCode: .SQLITE_MISUSE,
message: """
Mapping references missing column \(baseColumn). \
Valid column names are: \(columnNames.joined(separator: ", ")).
""")
}
let baseIndex = layout._layoutColumns[index].0
return (baseIndex, mappedColumn)
}
.sorted { $0.0 < $1.0 } // preserve ordering of base columns
return _LayoutedColumnMapping(layoutColumns: layoutColumns)
}
}
/// `SuffixRowAdapter` hides the leftmost columns in a row.
///
/// For example:
///
/// ```swift
/// let adapter = SuffixRowAdapter(fromIndex: 2)
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
///
/// // [c:2, d: 3]
/// try Row.fetchOne(db, sql: sql, adapter: adapter)!
/// ```
public struct SuffixRowAdapter: RowAdapter, Sendable {
/// The suffix index
let index: Int
/// Creates a SuffixRowAdapter that hides all columns before the
/// provided index.
///
/// If index is 0, the layout row is identical to the base row.
public init(fromIndex index: Int) {
GRDBPrecondition(index >= 0, "Negative column index is out of range")
self.index = index
}
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
_LayoutedColumnMapping(layoutColumns: layout._layoutColumns.suffix(from: index))
}
}
/// `RangeRowAdapter` is a row adapter that only exposes a range of columns.
///
/// For example:
///
/// ```swift
/// let adapter = RangeRowAdapter(1..<3)
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
///
/// // [b:1 c:2]
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
/// ```
public struct RangeRowAdapter: RowAdapter, Sendable {
/// The range
let range: CountableRange<Int>
/// Creates a RangeRowAdapter that only exposes a range of columns.
public init(_ range: CountableRange<Int>) {
GRDBPrecondition(range.lowerBound >= 0, "Negative column index is out of range")
self.range = range
}
/// Creates a RangeRowAdapter that only exposes a range of columns.
public init(_ range: CountableClosedRange<Int>) {
GRDBPrecondition(range.lowerBound >= 0, "Negative column index is out of range")
self.range = range.lowerBound..<(range.upperBound + 1)
}
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
_LayoutedColumnMapping(layoutColumns: layout._layoutColumns[range])
}
}
/// `ScopeAdapter` is a row adapter that defines row scopes.
///
/// `ScopeAdapter` does not change the columns and values of the fetched
/// row. Instead, it defines *scopes* based on other adapter, which you
/// access through the ``Row/scopes`` property of the fetched rows.
///
/// For example:
///
/// ```swift
/// let adapter = ScopeAdapter([
/// "left": RangeRowAdapter(0..<2),
/// "right": RangeRowAdapter(2..<4)])
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
///
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// row // [a:0 b:1 c:2 d:3]
/// row.scopes["left"] // [a:0 b:1]
/// row.scopes["right"] // [c:2 d:3]
/// row.scopes["missing"] // nil
/// ```
///
/// Scopes can be nested:
///
/// ```swift
/// let adapter = ScopeAdapter([
/// "left": ScopeAdapter([
/// "left": RangeRowAdapter(0..<1),
/// "right": RangeRowAdapter(1..<2)]),
/// "right": ScopeAdapter([
/// "left": RangeRowAdapter(2..<3),
/// "right": RangeRowAdapter(3..<4)])
/// ])
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// let leftRow = row.scopes["left"]!
/// leftRow.scopes["left"] // [a:0]
/// leftRow.scopes["right"] // [b:1]
///
/// let rightRow = row.scopes["right"]!
/// rightRow.scopes["left"] // [c:2]
/// rightRow.scopes["right"] // [d:3]
/// ```
///
/// Any adapter can be extended with scopes, with
/// ``RowAdapter/addingScopes(_:)``:
///
/// ```swift
/// let baseAdapter = RangeRowAdapter(0..<2)
/// let adapter = baseAdapter.addingScopes([
/// "remainder": SuffixRowAdapter(fromIndex: 2)
/// ])
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
///
/// row // [a:0 b:1]
/// row.scopes["remainder"] // [c:2 d:3]
/// ```
public struct ScopeAdapter: RowAdapter {
/// The base adapter
let base: any RowAdapter
/// The scope adapters
let scopes: [String: any RowAdapter]
/// Creates an adapter that preserves row contents and add scoped rows.
///
/// For example:
///
/// let adapter = ScopeAdapter(["suffix": SuffixRowAdapter(fromIndex: 1)])
/// let row = try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: adapter)!
/// row // [1, 2, 3]
/// row.scopes["suffix"] // [2, 3]
///
/// - parameter scopes: A dictionary that maps scope names to
/// row adapters.
public init(_ scopes: [String: any RowAdapter]) {
// Use SuffixRowAdapter(fromIndex: 0) as the identity adapter
self.init(base: SuffixRowAdapter(fromIndex: 0), scopes: scopes)
}
/// Creates an adapter based on the base adapter, and add scoped rows.
///
/// For example:
///
/// let baseAdapter = RangeRowAdapter(0..<1)
/// let adapter = ScopeAdapter(base: baseAdapter, scopes: ["suffix": SuffixRowAdapter(fromIndex: 1)])
/// let row = try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: adapter)!
/// row // [1]
/// row.scopes["initial"] // [2, 3]
///
/// If the base adapter already defines scopes, the given scopes replace
/// eventual existing scopes with the same name.
///
/// This initializer is equivalent to `baseAdapter.addingScopes(scopes)`.
///
/// - parameter base: A dictionary that maps scope names to
/// row adapters.
/// - parameter scopes: A dictionary that maps scope names to
/// row adapters.
public init(base: some RowAdapter, scopes: [String: any RowAdapter]) {
self.base = base
self.scopes = scopes
}
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
let layoutedAdapter = try base._layoutedAdapter(from: layout)
var layoutedScopes = layoutedAdapter._scopes
for (name, adapter) in scopes {
try layoutedScopes[name] = adapter._layoutedAdapter(from: layout)
}
return LayoutedScopeAdapter(
_mapping: layoutedAdapter._mapping,
_scopes: layoutedScopes)
}
}
/// The `_LayoutedRowAdapter` for `ScopeAdapter`
struct LayoutedScopeAdapter: _LayoutedRowAdapter {
let _mapping: _LayoutedColumnMapping
let _scopes: [String: any _LayoutedRowAdapter]
}
struct ChainedAdapter: RowAdapter {
let first: any RowAdapter
let second: any RowAdapter
func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
try second._layoutedAdapter(from: first._layoutedAdapter(from: layout)._mapping)
}
}
/// `RenameColumnAdapter` is a row adapter that renames columns.
///
/// For example:
///
/// ```swift
/// let adapter = RenameColumnAdapter { column in column + "rrr" }
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c"
///
/// // [arrr:0, brrr:1, crrr:2]
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
/// ```
///
/// This adapter is useful when subqueries contain duplicated column names:
///
/// ```swift
/// let sql = "SELECT * FROM (SELECT 1 AS id, 2 AS id)"
///
/// // Prints ["id", "id:1"]
/// // Note the "id:1" column, generated by SQLite.
/// let row = try Row.fetchOne(db, sql: sql)!
/// print(Array(row.columnNames))
///
/// // Drop the `:...` suffix, and prints ["id", "id"]
/// let adapter = RenameColumnAdapter { String($0.prefix(while: { $0 != ":" })) }
/// let adaptedRow = try Row.fetchOne(db, sql: sql, adapter: adapter)!
/// print(Array(adaptedRow.columnNames))
/// ```
public struct RenameColumnAdapter: RowAdapter {
let transform: (String) -> String
/// Creates a `RenameColumnAdapter` adapter that renames columns according to the
/// provided transform function.
public init(_ transform: @escaping (String) -> String) {
self.transform = transform
}
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
let layoutColumns = layout._layoutColumns.map { (index, column) in (index, transform(column)) }
return _LayoutedColumnMapping(layoutColumns: layoutColumns)
}
}
extension Row {
/// Creates a row from a base row and a statement adapter
convenience init(base: Row, adapter: some _LayoutedRowAdapter) {
self.init(impl: AdaptedRowImpl(base: base, adapter: adapter))
}
/// Returns self if adapter is nil
func adapted(with adapter: (any RowAdapter)?, layout: some _RowLayout) throws -> Row {
guard let adapter else {
return self
}
return try Row(base: self, adapter: adapter._layoutedAdapter(from: layout))
}
}
struct AdaptedRowImpl: RowImpl {
let base: Row
let adapter: any _LayoutedRowAdapter
let mapping: _LayoutedColumnMapping
init(base: Row, adapter: some _LayoutedRowAdapter) {
self.base = base
self.adapter = adapter
self.mapping = adapter._mapping
}
var count: Int { mapping._layoutColumns.count }
var isFetched: Bool { base.isFetched }
func scopes(prefetchedRows: Row.PrefetchedRowsView) -> Row.ScopesView {
Row.ScopesView(row: base, scopes: adapter._scopes, prefetchedRows: prefetchedRows)
}
func hasNull(atUncheckedIndex index: Int) -> Bool {
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
return base.impl.hasNull(atUncheckedIndex: mappedIndex)
}
func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue {
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
return base.impl.databaseValue(atUncheckedIndex: mappedIndex)
}
func fastDecode<Value: DatabaseValueConvertible & StatementColumnConvertible>(
_ type: Value.Type,
atUncheckedIndex index: Int)
throws -> Value
{
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
return try Value.fastDecode(fromRow: base, atUncheckedIndex: mappedIndex)
}
func withUnsafeData<T>(atUncheckedIndex index: Int, _ body: (Data?) throws -> T) throws -> T {
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
return try base.impl.withUnsafeData(atUncheckedIndex: mappedIndex, body)
}
func columnName(atUncheckedIndex index: Int) -> String {
mapping.columnName(atMappingIndex: index)
}
func index(forColumn name: String) -> Int? {
mapping._layoutIndex(ofColumn: name)
}
func copiedRow(_ row: Row) -> Row {
Row(base: base.copy(), adapter: adapter)
}
func unscopedRow(_ row: Row) -> Row {
assert(adapter._mapping._scopes.isEmpty)
return Row(base: base, adapter: adapter._mapping)
}
func unadaptedRow(_ row: Row) -> Row {
base.unadapted
}
}
@@ -0,0 +1,205 @@
/// A key that is used to decode a value in a row
@usableFromInline
enum RowKey: Hashable, Sendable {
/// A column name
case columnName(String)
/// A column index
case columnIndex(Int)
/// A scope
case scope(String)
/// A key of prefetched rows
case prefetchKey(String)
}
/// A decoding error
@usableFromInline
enum RowDecodingError: Error {
@usableFromInline
struct Context: CustomDebugStringConvertible, Sendable {
/// A description of what went wrong, for debugging purposes.
@usableFromInline
let debugDescription: String
let rowImpl: ArrayRowImpl // Sendable
/// The row that could not be decoded
var row: Row { Row(impl: rowImpl) }
/// Nil for RowDecodingError.keyNotFound, in order to avoid redundancy
let key: RowKey?
/// The SQL query
let sql: String?
/// The SQL query arguments
let statementArguments: StatementArguments?
init(decodingContext: RowDecodingContext, debugDescription: String) {
self.debugDescription = debugDescription
self.rowImpl = ArrayRowImpl(columns: decodingContext.row)
self.key = decodingContext.key
self.sql = decodingContext.sql
self.statementArguments = decodingContext.statementArguments
}
}
case keyNotFound(RowKey, Context)
case valueMismatch(Any.Type, Context)
var context: Context {
switch self {
case .keyNotFound(_, let context),
.valueMismatch(_, let context):
return context
}
}
/// Convenience method that builds the
/// `could not decode <Type> from database value <value>` error message.
static func valueMismatch(
_ type: Any.Type,
context: RowDecodingContext,
databaseValue: DatabaseValue)
-> Self
{
valueMismatch(
type,
RowDecodingError.Context(decodingContext: context, debugDescription: """
could not decode \(type) from database value \(databaseValue)
"""))
}
/// Convenience method that builds the
/// `could not decode <Type> from database value <value>` error message.
@usableFromInline
static func valueMismatch(
_ type: Any.Type,
sqliteStatement: SQLiteStatement,
index: CInt,
context: RowDecodingContext)
-> Self
{
valueMismatch(
type,
context: context,
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
}
/// Convenience method that builds the
/// `could not decode <Type> from database value <value>` error message.
static func valueMismatch(
_ type: Any.Type,
statement: Statement,
index: Int)
-> Self
{
valueMismatch(
type,
context: RowDecodingContext(statement: statement, index: index),
databaseValue: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: CInt(index)))
}
/// Convenience method that builds the `column not found: <column>`
/// error message.
@usableFromInline
static func columnNotFound(_ columnName: String, context: RowDecodingContext) -> Self {
keyNotFound(
.columnName(columnName),
RowDecodingError.Context(decodingContext: context, debugDescription: """
column not found: \(String(reflecting: columnName))
"""))
}
}
@usableFromInline
struct RowDecodingContext {
/// The row that is decoded
let row: Row
let key: RowKey?
/// The SQL query
let sql: String?
/// The SQL query arguments
let statementArguments: StatementArguments?
@usableFromInline
init(row: Row, key: RowKey? = nil) {
if let statement = row.statement {
self.key = key
self.row = row.copy()
self.sql = statement.sql
self.statementArguments = statement.arguments
} else if let sqliteStatement = row.sqliteStatement {
self.key = key
self.row = row.copy()
self.sql = String(cString: sqlite3_sql(sqliteStatement)).trimmedSQLStatement
self.statementArguments = nil // Can't rebuild them
} else {
self.key = key
self.row = row.copy()
self.sql = nil
self.statementArguments = nil
}
}
/// Convenience initializer
@usableFromInline
init(statement: Statement, index: Int) {
self.key = .columnIndex(index)
self.row = Row(copiedFromSQLiteStatement: statement.sqliteStatement, statement: statement)
self.sql = statement.sql
self.statementArguments = statement.arguments
}
}
extension RowDecodingError: CustomStringConvertible {
@usableFromInline
var description: String {
let context = self.context
let row = context.row
var chunks: [String] = []
if let key = context.key {
switch key {
case let .columnIndex(columnIndex):
let rowIndex = row.index(row.startIndex, offsetBy: columnIndex)
let columnName = row.columnNames[rowIndex]
chunks.append("column: \(String(reflecting: columnName))")
chunks.append("column index: \(columnIndex)")
case let .columnName(columnName):
if let columnIndex = row.index(forColumn: columnName) {
chunks.append("column: \(String(reflecting: columnName))")
chunks.append("column index: \(columnIndex)")
} else {
// column name is already mentioned in context.debugDescription
}
case .prefetchKey:
// key is already mentioned in context.debugDescription
break
case .scope:
// scope is already mentioned in context.debugDescription
break
}
}
chunks.append("row: \(row.description)")
if let sql = context.sql {
chunks.append("sql: `\(sql)`")
}
if let statementArguments = context.statementArguments {
chunks.append("arguments: \(statementArguments)")
}
return "\(context.debugDescription) - \(chunks.joined(separator: ", "))"
}
}
@@ -0,0 +1,387 @@
/// An SQL literal.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection. For example:
///
/// ```swift
/// try dbQueue.write { db in
/// let name: String = "O'Brien"
/// let id: Int64 = 42
/// let query: SQL = "UPDATE player SET name = \(name) WHERE id = \(id)"
///
/// // UPDATE player SET name = 'O''Brien' WHERE id = 42
/// try db.execute(literal: query)
/// }
/// ```
///
/// ## Topics
///
/// ### Creating an SQL Literal from a Literal Value
///
/// - ``init(stringLiteral:)``
/// - ``init(unicodeScalarLiteral:)-7p0al``
/// - ``init(extendedGraphemeClusterLiteral:)-1oham``
///
/// ### Creating an SQL Literal from an Interpolation
///
/// - ``init(stringInterpolation:)``
/// - ``SQLInterpolation``
///
/// ### Creating an SQL Literal from an SQL String
///
/// - ``init(sql:arguments:)``
///
/// ### Creating an SQL Literal from an SQL Expression
///
/// - ``init(_:)``
///
/// ### Concatenating SQL Literals
///
/// - ``append(literal:)``
/// - ``append(sql:arguments:)``
public struct SQL {
/// `SQL.Element` is a component of an `SQL` literal.
///
/// Elements can be qualified with table aliases, and this is how `SQL`
/// blends well in the query interface. See below how the `createdAt` column
/// is qualified with the `player` table in the generated SQL, in order to
/// avoid any conflict with the `team.createdAt` column:
///
/// func date(_ value: SQLSpecificExpressible) -> SQLExpression {
/// // An SQL literal made of three elements:
/// // - "DATE(" raw sql string
/// // - expression
/// // - ")" raw sql string
/// SQL("DATE(\(value))").sqlExpression
/// }
///
/// // SELECT player.*, team.*
/// // FROM player
/// // JOIN team ON team.id = player.teamId
/// // WHERE DATE(player.createdAt) = '2022-08-17'
/// let request = Player
/// .filter(date(Column("createdAt")) == "2022-08-17")
/// .including(required: Player.team)
enum Element {
/// A raw SQL literal with eventual arguments.
case sql(String, StatementArguments = StatementArguments())
/// A subquery.
case subquery(SQLSubquery)
/// An expression.
case expression(SQLExpression)
/// A selection.
case selection(SQLSelection)
/// An ordering.
case ordering(SQLOrdering)
var isEmpty: Bool {
switch self {
case let .sql(sql, _):
return sql.isEmpty
default:
// Subqueries, expressions, selections and orderings are
// assumed to be non-empty.
//
// Nothing prevents the user from creating an ill-formed empty
// expression, but we don't care about such misuse:
//
// // An ill-formed empty expression
// let expression = SQL("").sqlExpression
//
// let sql: SQL = "\(expression)"
// sql.isEmpty // false, deal with it ¯\_()_/¯
return false
}
}
fileprivate func sql(_ context: SQLGenerationContext) throws -> String {
switch self {
case let .sql(sql, arguments):
if context.append(arguments: arguments) == false {
// We don't know how to look for `?` in sql and
// replace them with literals.
fatalError("Not implemented: turning an SQL parameter into an SQL literal value")
}
return sql
case let .subquery(subquery):
return try subquery.sql(context)
case let .expression(expression):
return try expression.sql(context)
case let .selection(selection):
return try selection.sql(context)
case let .ordering(ordering):
return try ordering.sql(context)
}
}
fileprivate func qualified(with alias: TableAlias) -> Element {
switch self {
case .sql:
// A raw SQL string can't be qualified with a table alias,
// because we can't parse it.
return self
case .subquery:
// Subqueries don't need table alias
return self
case let .expression(expression):
return .expression(expression.qualified(with: alias))
case let .selection(selection):
return .selection(selection.qualified(with: alias))
case let .ordering(ordering):
return .ordering(ordering.qualified(with: alias))
}
}
}
private(set) var elements: [Element]
init(elements: [Element]) {
self.elements = elements
}
/// Creates an `SQL` literal from a plain SQL string, and
/// eventual arguments.
///
/// For example:
///
/// ```swift
/// let query = SQL(
/// sql: "UPDATE player SET name = ? WHERE id = ?",
/// arguments: [name, id])
/// ```
public init(sql: String, arguments: StatementArguments = StatementArguments()) {
self.init(elements: [.sql(sql, arguments)])
}
/// Creates an `SQL` literal from an SQL expression.
///
/// For example:
///
/// ```swift
/// let columnLiteral = SQL(Column("username"))
/// let suffixLiteral = SQL("@example.com".databaseValue)
/// let emailLiteral = [columnLiteral, suffixLiteral].joined(separator: " || ")
/// let request = User.select(emailLiteral.sqlExpression)
/// let emails = try String.fetchAll(db, request)
/// ```
public init(_ expression: some SQLSpecificExpressible) {
self.init(elements: [.expression(expression.sqlExpression)])
}
/// Returns true if this literal generates an empty SQL string
public var isEmpty: Bool {
elements.allSatisfy(\.isEmpty)
}
/// Turn a `SQL` literal into raw SQL and arguments.
///
/// - parameter db: A database connection.
/// - returns: A tuple made of a raw SQL string, and statement arguments.
public func build(_ db: Database) throws -> (sql: String, arguments: StatementArguments) {
let context = SQLGenerationContext(db)
let sql = try self.sql(context)
return (sql: sql, arguments: context.arguments)
}
/// Returns the literal SQL string given an SQL generation context.
func sql(_ context: SQLGenerationContext) throws -> String {
try elements.map { try $0.sql(context) }.joined()
}
func qualified(with alias: TableAlias) -> SQL {
SQL(elements: elements.map { $0.qualified(with: alias) })
}
}
extension SQL {
/// Returns the `SQL` literal produced by the concatenation of two literals.
///
/// For example:
///
/// ```swift
/// let name = "O'Brien"
/// let selection: SQL = "SELECT * FROM player "
/// let condition: SQL = "WHERE name = \(name)"
/// let query = selection + condition
/// ```
public static func + (lhs: SQL, rhs: SQL) -> SQL {
var result = lhs
result += rhs
return result
}
/// Appends an `SQL` literal to the receiver.
///
/// For example:
///
/// ```swift
/// let name = "O'Brien"
/// var query: SQL = "SELECT * FROM player "
/// query += "WHERE name = \(name)"
/// ```
public static func += (lhs: inout SQL, rhs: SQL) {
lhs.elements += rhs.elements
}
/// Appends an `SQL` literal to the receiver.
///
/// For example:
///
/// ```swift
/// let name = "O'Brien"
/// var query: SQL = "SELECT * FROM player "
/// query.append(literal: "WHERE name = \(name)")
/// ```
public mutating func append(literal sqlLiteral: SQL) {
self += sqlLiteral
}
/// Appends a plain SQL string to the receiver, and eventual arguments.
///
/// For example:
///
/// ```swift
/// let name = "O'Brien"
/// var query: SQL = "SELECT * FROM player "
/// query.append(sql: "WHERE name = ?", arguments: [name])
/// ```
public mutating func append(sql: String, arguments: StatementArguments = StatementArguments()) {
self += SQL(sql: sql, arguments: arguments)
}
}
extension SQL: SQLSpecificExpressible {
/// Creates a literal SQL expression.
///
/// Use this property when you need an explicit `SQLExpression`.
/// For example:
///
/// ```swift
/// func date(_ value: some SQLExpressible) -> SQLExpression {
/// SQL("DATE(\(value))").sqlExpression
/// }
///
/// // SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23'
/// let createdAt = Column("createdAt")
/// let request = Player.filter(date(createdAt) == "2020-01-23")
/// ```
public var sqlExpression: SQLExpression {
.literal(self)
}
}
extension SQL: SQLSelectable {
/// Creates a literal SQL result column.
///
/// Use this property when you need an explicit `SQLSelection`. For example:
///
/// ```swift
/// // SELECT firstName AS givenName, lastName AS familyName FROM player
/// let selection = SQL("firstName AS givenName, lastName AS familyName").sqlSelection
/// let request = Player.select(selection)
/// ```
public var sqlSelection: SQLSelection {
.literal(self)
}
}
extension SQL: SQLOrderingTerm {
/// Creates a literal SQL ordering term.
///
/// Use this property when you need an explicit `SQLOrdering`. For example:
///
/// ```swift
/// // SELECT * FROM player ORDER BY name DESC
/// let ordering = SQL("name DESC").sqlOrdering
/// let request = Player.order(ordering)
/// ```
///
/// An ordering term is only valid if it represent a single ordering terms.
///
/// ```swift
/// // Valid
/// SQL("score DESC").sqlOrdering
/// SQL("name").sqlOrdering
///
/// // Invalid
/// SQL("score DESC, name").sqlOrdering
/// ```
public var sqlOrdering: SQLOrdering {
.literal(self)
}
}
extension Sequence where Element == SQL {
/// Returns the concatenated `SQL` literal of this sequence of literals,
/// inserting the given raw SQL separator between each element.
///
/// For example:
///
/// ```
/// let components: [SQL] = [
/// "UPDATE player",
/// "SET name = \(name)",
/// "WHERE id = \(id)"
/// ]
/// let query = components.joined(separator: " ")
/// ```
///
/// - Note: The separator is a raw SQL string, not an ``SQL`` literal.
public func joined(separator: String = "") -> SQL {
if separator.isEmpty {
return SQL(elements: flatMap(\.elements))
} else {
return SQL(elements: Array(map(\.elements).joined(separator: CollectionOfOne(.sql(separator)))))
}
}
}
extension Collection where Element == SQL {
/// Returns the concatenated `SQL` literal of this collection of literals,
/// inserting the given raw SQL separator between each element.
///
/// For example:
///
/// ```swift
/// let components: [SQL] = [
/// "UPDATE player",
/// "SET name = \(name)",
/// "WHERE id = \(id)"
/// ]
/// let query = components.joined(separator: " ")
/// ```
///
/// - Note: The separator is a raw SQL string, not an ``SQL`` literal.
public func joined(separator: String = "") -> SQL {
if separator.isEmpty {
return SQL(elements: flatMap(\.elements))
} else {
return SQL(elements: Array(map(\.elements).joined(separator: CollectionOfOne(.sql(separator)))))
}
}
}
// MARK: - ExpressibleByStringInterpolation
extension SQL: ExpressibleByStringInterpolation {
public init(unicodeScalarLiteral: String) {
self.init(sql: unicodeScalarLiteral, arguments: [])
}
public init(extendedGraphemeClusterLiteral: String) {
self.init(sql: extendedGraphemeClusterLiteral, arguments: [])
}
/// Creates an `SQL` literal from the given literal SQL string.
public init(stringLiteral: String) {
self.init(sql: stringLiteral, arguments: [])
}
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
self.init(elements: sqlInterpolation.elements)
}
}
@@ -0,0 +1,51 @@
public struct SQLInterpolation: StringInterpolationProtocol {
var elements: [SQL.Element]
public init(literalCapacity: Int, interpolationCount: Int) {
elements = []
elements.reserveCapacity(interpolationCount + 1)
}
public mutating func appendLiteral(_ sql: String) {
if sql.isEmpty { return }
elements.append(.sql(sql))
}
/// Appends a raw SQL snippet, with eventual arguments.
///
/// For example:
///
/// "SELECT * FROM \(sql: "player")"
/// "SELECT * FROM player WHERE \(sql: "name = ?", arguments: ["O'Brien"])"
public mutating func appendInterpolation(sql: String, arguments: StatementArguments = StatementArguments()) {
elements.append(.sql(sql, arguments))
}
/// Appends a raw `SQL` literal.
///
/// For example:
///
/// "SELECT * FROM \(SQL("player"))"
/// "SELECT * FROM player WHERE \(SQL("name = \("O'Brien")"))"
public mutating func appendInterpolation(_ sqlLiteral: SQL) {
elements.append(contentsOf: sqlLiteral.elements)
}
/// Appends a String expression.
///
/// For example:
///
/// "SELECT * FROM player WHERE name = \("O'Brien")"
public mutating func appendInterpolation<S: StringProtocol>(_ string: S) {
elements.append(.expression(String(string).sqlExpression))
}
/// Appends a raw `SQL` literal.
///
/// For example:
///
/// "SELECT * FROM player WHERE \(literal: "name = \("O'Brien")")"
public mutating func appendInterpolation(literal sqlLiteral: SQL) {
elements.append(contentsOf: sqlLiteral.elements)
}
}
@@ -0,0 +1,236 @@
/// An SQL request that can decode database rows.
///
/// `SQLRequest` allows you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// extension Player: FetchableRecord {
/// static func filter(name: String) -> SQLRequest<Player> {
/// "SELECT * FROM player WHERE name = \(name)"
/// }
///
/// static func maximumScore() -> SQLRequest<Int> {
/// "SELECT MAX(score) FROM player"
/// }
/// }
///
/// try dbQueue.read { db in
/// let players = try Player.filter(name: "O'Brien").fetchAll(db) // [Player]
/// let maxScore = try Player.maximumScore().fetchOne(db) // Int?
/// }
/// ```
///
/// An `SQLRequest` can be created from a string literal or interpolation, as in
/// the above examples, and from the initializers documented below.
///
/// ## Topics
///
/// ### Creating an SQL Request from a Literal Value
///
/// - ``init(stringLiteral:)``
/// - ``init(unicodeScalarLiteral:)-84mq8``
/// - ``init(extendedGraphemeClusterLiteral:)-1sf75``
///
/// ### Creating an SQL Request from an Interpolation
///
/// - ``init(stringInterpolation:)``
///
/// ### Creating an SQL Request from an SQL Literal
///
/// - ``init(literal:adapter:cached:)-4vuxn``
/// - ``init(literal:adapter:cached:)-82f97``
///
/// ### Creating an SQL Request from an SQL String
///
/// - ``init(sql:arguments:adapter:cached:)-3qq8t``
/// - ``init(sql:arguments:adapter:cached:)-5ecx2``
public struct SQLRequest<RowDecoder> {
/// There are two statement caches: one "public" for statements generated by
/// the user, and one "internal" for the statements generated by GRDB. Those
/// are separated so that GRDB has no opportunity to inadvertently modify
/// the arguments of user's cached statements.
enum Cache {
/// The public cache, for library user
case `public`
/// The internal cache, for GRDB
case `internal`
}
/// The row adapter.
public var adapter: (any RowAdapter)?
private(set) var sqlLiteral: SQL
let cache: Cache?
private init(
literal sqlLiteral: SQL,
adapter: (any RowAdapter)?,
fromCache cache: Cache?,
type: RowDecoder.Type)
{
self.sqlLiteral = sqlLiteral
self.adapter = adapter
self.cache = cache
}
}
extension SQLRequest {
/// Creates a request from an SQL string.
///
/// ```swift
/// let request = SQLRequest<String>(sql: """
/// SELECT name FROM player
/// """)
/// let request = SQLRequest<Player>(sql: """
/// SELECT * FROM player WHERE name = ?
/// """, arguments: ["O'Brien"])
/// ```
///
/// - parameters:
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
public init(
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil,
cached: Bool = false)
{
self.init(
literal: SQL(sql: sql, arguments: arguments),
adapter: adapter,
fromCache: cached ? .public : nil,
type: RowDecoder.self)
}
/// Creates a request from an ``SQL`` literal.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// let name = "O'Brien"
/// let request = SQLRequest<Player>(literal: """
/// SELECT * FROM player WHERE name = \(name)
/// """)
/// ```
///
/// - parameters:
/// - sqlLiteral: An `SQL` literal.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
public init(literal sqlLiteral: SQL, adapter: (any RowAdapter)? = nil, cached: Bool = false) {
self.init(
literal: sqlLiteral,
adapter: adapter,
fromCache: cached ? .public : nil,
type: RowDecoder.self)
}
}
extension SQLRequest<Row> {
/// Creates a request of database rows, from an SQL string.
///
/// ```swift
/// let request = SQLRequest(sql: """
/// SELECT * FROM player WHERE name = ?
/// """, arguments: ["O'Brien"])
/// ```
///
/// - parameters:
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
public init(
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil,
cached: Bool = false)
{
self.init(
literal: SQL(sql: sql, arguments: arguments),
adapter: adapter,
fromCache: cached ? .public : nil,
type: Row.self)
}
/// Creates a request of database rows, from an ``SQL`` literal.
///
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
/// without any risk of syntax errors or SQL injection:
///
/// ```swift
/// let name = "O'Brien"
/// let request = SQLRequest(literal: """
/// SELECT * FROM player WHERE name = \(name)
/// """)
/// ```
///
/// - parameters:
/// - sqlLiteral: An `SQL` literal.
/// - adapter: Optional RowAdapter.
/// - cached: Defaults to false. If true, the request reuses a cached
/// prepared statement.
public init(literal sqlLiteral: SQL, adapter: (any RowAdapter)? = nil, cached: Bool = false) {
self.init(
literal: sqlLiteral,
adapter: adapter,
fromCache: cached ? .public : nil,
type: Row.self)
}
}
extension SQLRequest: FetchRequest {
public var sqlSubquery: SQLSubquery {
.literal(sqlLiteral)
}
public func fetchCount(_ db: Database) throws -> Int {
try SQLRequest<Int>("SELECT COUNT(*) FROM (\(self))").fetchOne(db)!
}
public func makePreparedRequest(
_ db: Database,
forSingleResult singleResult: Bool = false)
throws -> PreparedRequest
{
let context = SQLGenerationContext(db)
let sql = try sqlLiteral.sql(context)
let statement: Statement
switch cache {
case .none:
statement = try db.makeStatement(sql: sql)
case .public:
statement = try db.cachedStatement(sql: sql)
case .internal:
statement = try db.internalCachedStatement(sql: sql)
}
try statement.setArguments(context.arguments)
return PreparedRequest(statement: statement, adapter: adapter)
}
}
extension SQLRequest: ExpressibleByStringInterpolation {
public init(unicodeScalarLiteral: String) {
self.init(sql: unicodeScalarLiteral)
}
public init(extendedGraphemeClusterLiteral: String) {
self.init(sql: extendedGraphemeClusterLiteral)
}
/// Creates an `SQLRequest` from the given literal SQL string.
public init(stringLiteral: String) {
self.init(sql: stringLiteral)
}
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
self.init(literal: SQL(stringInterpolation: sqlInterpolation))
}
}
@@ -0,0 +1,68 @@
import Dispatch
/// SchedulingWatchdog makes sure that databases connections are used on correct
/// dispatch queues, and warns the user with a fatal error whenever she misuses
/// a database connection.
///
/// Generally speaking, each connection has its own dispatch queue. But it's not
/// enough: users need to use two database connections at the same time:
/// <https://github.com/groue/GRDB.swift/issues/55>. To support this use case, a
/// single dispatch queue can be temporarily shared by two or more connections.
///
/// - SchedulingWatchdog.makeSerializedQueue(allowingDatabase:) creates a
/// dispatch queue that allows one database.
///
/// It does so by registering one instance of SchedulingWatchdog as a specific
/// of the dispatch queue, a SchedulingWatchdog that allows that database only.
///
/// Later on, the queue can be shared by several databases with the method
/// inheritingAllowedDatabases(from:execute:). See SerializedDatabase.sync()
/// for an example.
///
/// - preconditionValidQueue() crashes whenever a database is used in an invalid
/// dispatch queue.
final class SchedulingWatchdog {
private static let watchDogKey = DispatchSpecificKey<SchedulingWatchdog>()
private(set) var allowedDatabases: [Database]
var databaseObservationBroker: DatabaseObservationBroker?
private init(allowedDatabase database: Database) {
allowedDatabases = [database]
}
static func allowDatabase(_ database: Database, onQueue queue: DispatchQueue) {
precondition(queue.getSpecific(key: watchDogKey) == nil)
let watchdog = SchedulingWatchdog(allowedDatabase: database)
queue.setSpecific(key: watchDogKey, value: watchdog)
}
func inheritingAllowedDatabases<T>(from other: SchedulingWatchdog, execute body: () throws -> T) rethrows -> T {
let backup = allowedDatabases
allowedDatabases.append(contentsOf: other.allowedDatabases)
defer { allowedDatabases = backup }
return try body()
}
static func preconditionValidQueue(
_ db: Database,
_ message: @autoclosure() -> String = "Database was not used on the correct thread.",
file: StaticString = #file,
line: UInt = #line)
{
GRDBPrecondition(allows(db), message(), file: file, line: line)
}
/// Returns whether the database argument can be used in the current
/// dispatch queue.
static func allows(_ db: Database) -> Bool {
current?.allows(db) ?? false
}
static var current: SchedulingWatchdog? {
DispatchQueue.getSpecific(key: watchDogKey)
}
func allows(_ db: Database) -> Bool {
allowedDatabases.contains { $0 === db }
}
}
@@ -0,0 +1,288 @@
import Foundation
/// A class that serializes accesses to an SQLite connection.
final class SerializedDatabase {
/// The database connection
private let db: Database
/// The database configuration
var configuration: Configuration { db.configuration }
/// The path to the database file
let path: String
/// The dispatch queue
private let queue: DispatchQueue
/// If true, overrides `configuration.allowsUnsafeTransactions`.
private var allowsUnsafeTransactions = false
init(
path: String,
configuration: Configuration = Configuration(),
defaultLabel: String,
purpose: String? = nil)
throws
{
// According to https://www.sqlite.org/threadsafe.html
//
// > SQLite support three different threading modes:
// >
// > 1. Multi-thread. In this mode, SQLite can be safely used by
// > multiple threads provided that no single database connection is
// > used simultaneously in two or more threads.
// >
// > 2. Serialized. In serialized mode, SQLite can be safely used by
// > multiple threads with no restriction.
// >
// > [...]
// >
// > The default mode is serialized.
//
// Since our database connection is only used via our serial dispatch
// queue, there is no purpose using the default serialized mode.
var config = configuration
config.threadingMode = .multiThread
self.path = path
let identifier = configuration.identifier(defaultLabel: defaultLabel, purpose: purpose)
self.db = try Database(
path: path,
description: identifier,
configuration: config)
if config.readonly {
self.queue = configuration.makeReaderDispatchQueue(label: identifier)
} else {
self.queue = configuration.makeWriterDispatchQueue(label: identifier)
}
SchedulingWatchdog.allowDatabase(db, onQueue: queue)
try queue.sync {
do {
try db.setUp()
} catch {
// Recent versions of the Swift compiler will call the
// deinitializer. Older ones won't.
// See https://bugs.swift.org/browse/SR-13746 for details.
//
// So let's close the database now. The deinitializer
// will only close the database if needed.
db.close_v2()
throw error
}
}
}
deinit {
// Database may be deallocated in its own queue: allow reentrancy
reentrantSync { db in
db.close_v2()
}
}
/// Executes database operations, returns their result after they have
/// finished executing, and allows or forbids long-lived transactions.
///
/// This method is not reentrant.
///
/// - parameter allowingLongLivedTransaction: When true, the
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
/// ignored until this method is called again with false.
func sync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
try sync { db in
self.allowsUnsafeTransactions = allowingLongLivedTransaction
return try body(db)
}
}
/// Executes database operations, and returns their result after they
/// have finished executing.
///
/// This method is not reentrant.
func sync<T>(_ block: (Database) throws -> T) rethrows -> T {
// Three different cases:
//
// 1. A database is invoked from some queue like the main queue:
//
// serializedDatabase.sync { db in // <-- we're here
// }
//
// 2. A database is invoked in a reentrant way:
//
// serializedDatabase.sync { db in
// serializedDatabase.sync { db in // <-- we're here
// }
// }
//
// 3. A database in invoked from another database:
//
// serializedDatabase1.sync { db1 in
// serializedDatabase2.sync { db2 in // <-- we're here
// }
// }
guard let watchdog = SchedulingWatchdog.current else {
// Case 1
return try queue.sync {
defer { preconditionNoUnsafeTransactionLeft(db) }
return try block(db)
}
}
// Case 2 is forbidden.
GRDBPrecondition(!watchdog.allows(db), "Database methods are not reentrant.")
// Case 3
return try queue.sync {
try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) {
defer { preconditionNoUnsafeTransactionLeft(db) }
return try block(db)
}
}
}
/// Executes database operations, returns their result after they have
/// finished executing, and allows or forbids long-lived transactions.
///
/// This method is reentrant.
///
/// - parameter allowingLongLivedTransaction: When true, the
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
/// ignored until this method is called again with false.
func reentrantSync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
try reentrantSync { db in
self.allowsUnsafeTransactions = allowingLongLivedTransaction
return try body(db)
}
}
/// Executes database operations, and returns their result after they
/// have finished executing.
///
/// This method is reentrant.
func reentrantSync<T>(_ block: (Database) throws -> T) rethrows -> T {
// Three different cases:
//
// 1. A database is invoked from some queue like the main queue:
//
// serializedDatabase.reentrantSync { db in // <-- we're here
// }
//
// 2. A database is invoked in a reentrant way:
//
// serializedDatabase.reentrantSync { db in
// serializedDatabase.reentrantSync { db in // <-- we're here
// }
// }
//
// 3. A database in invoked from another database:
//
// serializedDatabase1.reentrantSync { db1 in
// serializedDatabase2.reentrantSync { db2 in // <-- we're here
// }
// }
guard let watchdog = SchedulingWatchdog.current else {
// Case 1
return try queue.sync {
// Since we are reentrant, a transaction may already be opened.
// In this case, don't check for unsafe transaction at the end.
if db.isInsideTransaction {
return try block(db)
} else {
defer { preconditionNoUnsafeTransactionLeft(db) }
return try block(db)
}
}
}
// Case 2
if watchdog.allows(db) {
// Since we are reentrant, a transaction may already be opened.
// In this case, don't check for unsafe transaction at the end.
if db.isInsideTransaction {
return try block(db)
} else {
defer { preconditionNoUnsafeTransactionLeft(db) }
return try block(db)
}
}
// Case 3
return try queue.sync {
try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) {
// Since we are reentrant, a transaction may already be opened.
// In this case, don't check for unsafe transaction at the end.
if db.isInsideTransaction {
return try block(db)
} else {
defer { preconditionNoUnsafeTransactionLeft(db) }
return try block(db)
}
}
}
}
/// Schedules database operations for execution, and returns immediately.
func async(_ block: @escaping (Database) -> Void) {
queue.async {
block(self.db)
self.preconditionNoUnsafeTransactionLeft(self.db)
}
}
/// Returns true if any only if the current dispatch queue is valid.
var onValidQueue: Bool {
SchedulingWatchdog.current?.allows(db) ?? false
}
/// Executes the block in the current queue.
///
/// - precondition: the current dispatch queue is valid.
func execute<T>(_ block: (Database) throws -> T) rethrows -> T {
preconditionValidQueue()
return try block(db)
}
func interrupt() {
// Intentionally not scheduled in our serial queue
db.interrupt()
}
func suspend() {
// Intentionally not scheduled in our serial queue
db.suspend()
}
func resume() {
// Intentionally not scheduled in our serial queue
db.resume()
}
/// Fatal error if current dispatch queue is not valid.
func preconditionValidQueue(
_ message: @autoclosure() -> String = "Database was not used on the correct thread.",
file: StaticString = #file,
line: UInt = #line)
{
SchedulingWatchdog.preconditionValidQueue(db, message(), file: file, line: line)
}
/// Fatal error if a transaction has been left opened.
private func preconditionNoUnsafeTransactionLeft(
_ db: Database,
_ message: @autoclosure() -> String = "A transaction has been left opened at the end of a database access",
file: StaticString = #file,
line: UInt = #line)
{
GRDBPrecondition(
allowsUnsafeTransactions || configuration.allowsUnsafeTransactions || !db.isInsideTransaction,
message(),
file: file,
line: line)
}
}
// @unchecked because the wrapped `Database` itself is not Sendable.
// It happens the job of SerializedDatabase is precisely to provide thread-safe
// access to `Database`.
extension SerializedDatabase: @unchecked Sendable { }
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
#if canImport(string_h)
import string_h
#elseif os(Linux)
import Glibc
#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)
import Darwin
#endif
/// `StatementAuthorizer` provides information about compiled database
/// statements, and prevents the truncate optimization when row deletions are
/// observed by transaction observers.
///
/// <https://www.sqlite.org/c3ref/set_authorizer.html>
/// <https://www.sqlite.org/lang_delete.html#the_truncate_optimization>
final class StatementAuthorizer {
private unowned var database: Database
/// What a statement reads.
var selectedRegion = DatabaseRegion()
/// What a statement writes.
var databaseEventKinds: [DatabaseEventKind] = []
/// True if a statement alters the schema in a way that requires
/// invalidation of the schema cache. For example, adding a column to a
/// table invalidates the schema cache.
var invalidatesDatabaseSchemaCache = false
/// Not nil if a statement is a BEGIN/COMMIT/ROLLBACK/RELEASE transaction or
/// savepoint statement.
var transactionEffect: Statement.TransactionEffect?
private var isDropStatement = false
init(_ database: Database) {
self.database = database
}
/// Registers the authorizer with `sqlite3_set_authorizer`.
func register() {
let authorizerP = Unmanaged.passUnretained(self).toOpaque()
sqlite3_set_authorizer(
database.sqliteConnection,
{ (authorizerP, actionCode, cString1, cString2, cString3, cString4) in
Unmanaged<StatementAuthorizer>
.fromOpaque(authorizerP.unsafelyUnwrapped)
.takeUnretainedValue()
.authorize(actionCode, cString1, cString2, cString3, cString4)
},
authorizerP)
}
/// Reset before compiling a new statement
func reset() {
selectedRegion = DatabaseRegion()
databaseEventKinds = []
invalidatesDatabaseSchemaCache = false
transactionEffect = nil
isDropStatement = false
}
private func authorize(
_ actionCode: CInt,
_ cString1: UnsafePointer<CChar>?,
_ cString2: UnsafePointer<CChar>?,
_ cString3: UnsafePointer<CChar>?,
_ cString4: UnsafePointer<CChar>?)
-> CInt
{
// Uncomment when debugging
// print("""
// StatementAuthorizer: \
// \(AuthorizerActionCode(rawValue: actionCode)) \
// \([cString1, cString2, cString3, cString4].compactMap { $0.map(String.init) }.joined(separator: ", "))
// """)
switch actionCode {
case SQLITE_DROP_TABLE, SQLITE_DROP_VTABLE, SQLITE_DROP_TEMP_TABLE,
SQLITE_DROP_INDEX, SQLITE_DROP_TEMP_INDEX,
SQLITE_DROP_VIEW, SQLITE_DROP_TEMP_VIEW,
SQLITE_DROP_TRIGGER, SQLITE_DROP_TEMP_TRIGGER:
isDropStatement = true
invalidatesDatabaseSchemaCache = true
return SQLITE_OK
case SQLITE_ATTACH, SQLITE_DETACH, SQLITE_ALTER_TABLE,
SQLITE_CREATE_INDEX, SQLITE_CREATE_TABLE,
SQLITE_CREATE_TEMP_INDEX, SQLITE_CREATE_TEMP_TABLE,
SQLITE_CREATE_TEMP_TRIGGER, SQLITE_CREATE_TEMP_VIEW,
SQLITE_CREATE_TRIGGER, SQLITE_CREATE_VIEW,
SQLITE_CREATE_VTABLE:
invalidatesDatabaseSchemaCache = true
return SQLITE_OK
case SQLITE_READ:
guard let tableName = cString1.map(String.init) else { return SQLITE_OK }
guard let columnName = cString2.map(String.init) else { return SQLITE_OK }
if columnName.isEmpty {
// SELECT COUNT(*) FROM table
selectedRegion.formUnion(DatabaseRegion(table: tableName))
} else {
// SELECT column FROM table
selectedRegion.formUnion(DatabaseRegion(table: tableName, columns: [columnName]))
}
return SQLITE_OK
case SQLITE_INSERT:
guard let tableName = cString1.map(String.init) else { return SQLITE_OK }
databaseEventKinds.append(.insert(tableName: tableName))
return SQLITE_OK
case SQLITE_DELETE:
if isDropStatement { return SQLITE_OK }
guard let cString1 else { return SQLITE_OK }
// Deletions from sqlite_master and sqlite_temp_master are not like
// other deletions: `sqlite3_update_hook` does not notify them, and
// they are prevented when the truncate optimization is disabled.
// Let's always authorize such deletions by returning SQLITE_OK:
guard strcmp(cString1, "sqlite_master") != 0 else { return SQLITE_OK }
guard strcmp(cString1, "sqlite_temp_master") != 0 else { return SQLITE_OK }
let tableName = String(cString: cString1)
databaseEventKinds.append(.delete(tableName: tableName))
if let observationBroker = database.observationBroker,
observationBroker.observesDeletions(on: tableName)
{
// Prevent the truncate optimization so that
// `sqlite3_update_hook` notifies individual row deletions to
// transaction observers.
return SQLITE_IGNORE
} else {
return SQLITE_OK
}
case SQLITE_UPDATE:
guard let tableName = cString1.map(String.init) else { return SQLITE_OK }
guard let columnName = cString2.map(String.init) else { return SQLITE_OK }
insertUpdateEventKind(tableName: tableName, columnName: columnName)
return SQLITE_OK
case SQLITE_TRANSACTION:
guard let cString1 else { return SQLITE_OK }
if strcmp(cString1, "BEGIN") == 0 {
transactionEffect = .beginTransaction
} else if strcmp(cString1, "COMMIT") == 0 {
transactionEffect = .commitTransaction
} else if strcmp(cString1, "ROLLBACK") == 0 {
transactionEffect = .rollbackTransaction
}
return SQLITE_OK
case SQLITE_SAVEPOINT:
guard let cString1 else { return SQLITE_OK }
guard let name = cString2.map(String.init) else { return SQLITE_OK }
if strcmp(cString1, "BEGIN") == 0 {
transactionEffect = .beginSavepoint(name)
} else if strcmp(cString1, "RELEASE") == 0 {
transactionEffect = .releaseSavepoint(name)
} else if strcmp(cString1, "ROLLBACK") == 0 {
transactionEffect = .rollbackSavepoint(name)
}
return SQLITE_OK
case SQLITE_FUNCTION:
// SQLite 3.37.2 does not report ALTER TABLE DROP COLUMN with the
// SQLITE_ALTER_TABLE action code. SQLite 3.38 does.
//
// Until SQLite 3.38, we need to find another way to set the
// `invalidatesDatabaseSchemaCache` flag for such
// statement, and it is SQLITE_FUNCTION sqlite_drop_column.
//
// See <https://github.com/groue/GRDB.swift/pull/1144#issuecomment-1015155717>
// See <https://sqlite.org/forum/forumpost/bd47580ec2>
if sqlite3_libversion_number() < 3038000,
let cString2,
strcmp(cString2, "sqlite_drop_column") == 0
{
invalidatesDatabaseSchemaCache = true
}
return SQLITE_OK
default:
return SQLITE_OK
}
}
private func insertUpdateEventKind(tableName: String, columnName: String) {
for (index, eventKind) in databaseEventKinds.enumerated() {
if case .update(let t, let columnNames) = eventKind, t == tableName {
var columnNames = columnNames
columnNames.insert(columnName)
databaseEventKinds[index] = .update(tableName: tableName, columnNames: columnNames)
return
}
}
databaseEventKinds.append(.update(tableName: tableName, columnNames: [columnName]))
}
}
private struct AuthorizerActionCode: RawRepresentable, CustomStringConvertible {
let rawValue: CInt
var description: String {
switch rawValue {
case 1: return "SQLITE_CREATE_INDEX"
case 2: return "SQLITE_CREATE_TABLE"
case 3: return "SQLITE_CREATE_TEMP_INDEX"
case 4: return "SQLITE_CREATE_TEMP_TABLE"
case 5: return "SQLITE_CREATE_TEMP_TRIGGER"
case 6: return "SQLITE_CREATE_TEMP_VIEW"
case 7: return "SQLITE_CREATE_TRIGGER"
case 8: return "SQLITE_CREATE_VIEW"
case 9: return "SQLITE_DELETE"
case 10: return "SQLITE_DROP_INDEX"
case 11: return "SQLITE_DROP_TABLE"
case 12: return "SQLITE_DROP_TEMP_INDEX"
case 13: return "SQLITE_DROP_TEMP_TABLE"
case 14: return "SQLITE_DROP_TEMP_TRIGGER"
case 15: return "SQLITE_DROP_TEMP_VIEW"
case 16: return "SQLITE_DROP_TRIGGER"
case 17: return "SQLITE_DROP_VIEW"
case 18: return "SQLITE_INSERT"
case 19: return "SQLITE_PRAGMA"
case 20: return "SQLITE_READ"
case 21: return "SQLITE_SELECT"
case 22: return "SQLITE_TRANSACTION"
case 23: return "SQLITE_UPDATE"
case 24: return "SQLITE_ATTACH"
case 25: return "SQLITE_DETACH"
case 26: return "SQLITE_ALTER_TABLE"
case 27: return "SQLITE_REINDEX"
case 28: return "SQLITE_ANALYZE"
case 29: return "SQLITE_CREATE_VTABLE"
case 30: return "SQLITE_DROP_VTABLE"
case 31: return "SQLITE_FUNCTION"
case 32: return "SQLITE_SAVEPOINT"
case 0: return "SQLITE_COPY"
case 33: return "SQLITE_RECURSIVE"
default: return "\(rawValue)"
}
}
}
@@ -0,0 +1,847 @@
/// A type that can decode itself from the low-level C interface to
/// SQLite results.
///
/// `StatementColumnConvertible` is adopted by `Bool`, `Int`, `String`,
/// `Date`, and most common values.
///
/// When a type conforms to both ``DatabaseValueConvertible`` and
/// `StatementColumnConvertible`, GRDB can apply some optimization whenever
/// direct access to SQLite is possible. For example:
///
/// ```swift
/// // Optimized
/// let scores = Int.fetchAll(db, sql: "SELECT score FROM player")
///
/// let rows = try Row.fetchCursor(db, sql: "SELECT * FROM player")
/// while let row = try rows.next() {
/// // Optimized
/// let int: Int = row[0]
/// let name: String = row[1]
/// }
///
/// struct Player: FetchableRecord {
/// var name: String
/// var score: Int
///
/// init(row: Row) {
/// // Optimized
/// name = row["name"]
/// score = row["score"]
/// }
/// }
/// ```
///
/// To conform to `StatementColumnConvertible`, provide a custom implementation
/// of ``init(sqliteStatement:index:)-354je``. This implementation is ready-made
/// for `RawRepresentable` types whose `RawValue`
/// is `StatementColumnConvertible`.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/column_blob.html>
///
/// ## Topics
///
/// ### Creating a Value
///
/// - ``init(sqliteStatement:index:)-354je``
/// - ``fromStatement(_:atUncheckedIndex:)-2i8y6``
///
/// ### Fetching Values from Raw SQL
///
/// - ``DatabaseValueConvertible/fetchCursor(_:sql:arguments:adapter:)-4xfxh``
/// - ``DatabaseValueConvertible/fetchAll(_:sql:arguments:adapter:)-7bn2i``
/// - ``DatabaseValueConvertible/fetchSet(_:sql:arguments:adapter:)-1ythd``
/// - ``DatabaseValueConvertible/fetchOne(_:sql:arguments:adapter:)-563lc``
///
/// ### Fetching Values from a Prepared Statement
///
/// - ``DatabaseValueConvertible/fetchCursor(_:arguments:adapter:)-81f9d``
/// - ``DatabaseValueConvertible/fetchAll(_:arguments:adapter:)-64gua``
/// - ``DatabaseValueConvertible/fetchSet(_:arguments:adapter:)-9fh2b``
/// - ``DatabaseValueConvertible/fetchOne(_:arguments:adapter:)-8cbzp``
///
/// ### Fetching Values from a Request
///
/// - ``DatabaseValueConvertible/fetchCursor(_:_:)-77a34``
/// - ``DatabaseValueConvertible/fetchAll(_:_:)-7tnun``
/// - ``DatabaseValueConvertible/fetchSet(_:_:)-4bc1m``
/// - ``DatabaseValueConvertible/fetchOne(_:_:)-94q4e``
///
/// ### Supporting Types
///
/// - ``FastDatabaseValueCursor``
public protocol StatementColumnConvertible {
/// Creates an instance from a raw SQLite statement pointer, if possible.
///
/// This method can be called with a NULL database value.
///
/// - warning: Do not customize the default implementation.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
/// - returns: A decoded value, or, if decoding is impossible, nil.
static func fromStatement(
_ sqliteStatement: SQLiteStatement,
atUncheckedIndex index: CInt)
-> Self?
/// Creates an instance from a raw SQLite statement pointer, if possible.
///
/// Do not check for `NULL` in your implementation of this method. Null
/// database values are handled
/// in ``StatementColumnConvertible/fromStatement(_:atUncheckedIndex:)-2i8y6``.
///
/// For example, here is the how Int64 adopts StatementColumnConvertible:
///
/// ```swift
/// extension Int64: StatementColumnConvertible {
/// public init(sqliteStatement: SQLiteStatement, index: CInt) {
/// self = sqlite3_column_int64(sqliteStatement, index)
/// }
/// }
/// ```
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/column_blob.html>
///
/// - precondition: This initializer is not called with a NULL
/// database value.
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
/// - returns: A decoded value, or, if decoding is impossible, nil.
init?(sqliteStatement: SQLiteStatement, index: CInt)
}
extension StatementColumnConvertible {
// `Optional` overrides this default behavior.
/// Default implementation fails on decoding NULL.
@inline(__always)
@inlinable
public static func fromStatement(_ sqliteStatement: SQLiteStatement, atUncheckedIndex index: CInt) -> Self? {
if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
return nil
}
return self.init(sqliteStatement: sqliteStatement, index: index)
}
}
// MARK: - Conversions
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
@usableFromInline
/* private */ static func _valueMismatch(
fromStatement sqliteStatement: SQLiteStatement,
atUncheckedIndex index: CInt,
context: @autoclosure () -> RowDecodingContext)
throws -> Never
{
throw RowDecodingError.valueMismatch(
Self.self,
sqliteStatement: sqliteStatement,
index: index,
context: context())
}
@inline(__always)
@inlinable
static func fastDecode(
fromRow row: Row,
atUncheckedIndex index: Int)
throws -> Self
{
if let sqliteStatement = row.sqliteStatement {
return try fastDecode(
fromStatement: sqliteStatement,
atUncheckedIndex: CInt(index),
context: RowDecodingContext(row: row, key: .columnIndex(index)))
}
// Support for fast decoding from adapted rows
return try row.fastDecode(Self.self, atUncheckedIndex: index)
}
@inline(__always)
@inlinable
static func fastDecode(
fromStatement sqliteStatement: SQLiteStatement,
atUncheckedIndex index: CInt,
context: @autoclosure () -> RowDecodingContext)
throws -> Self
{
if let value = fromStatement(sqliteStatement, atUncheckedIndex: index) {
return value
} else {
try _valueMismatch(fromStatement: sqliteStatement, atUncheckedIndex: index, context: context())
}
}
// Support for Decodable
@inline(__always)
@inlinable
static func fastDecodeIfPresent(
fromRow row: Row,
atUncheckedIndex index: Int)
throws -> Self?
{
try Optional<Self>.fastDecode(fromRow: row, atUncheckedIndex: index)
}
}
// MARK: - Cursors
/// A cursor of database values.
///
/// A `FastDatabaseValueCursor` iterates all rows from a database request. Its
/// elements are the database values decoded from the leftmost column.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let names: FastDatabaseValueCursor<String> = try String.fetchCursor(db, sql: """
/// SELECT name FROM player
/// """)
/// while let name = names.next() { // String
/// print(name)
/// }
/// }
/// ```
public final class FastDatabaseValueCursor<Value>: DatabaseCursor
where Value: DatabaseValueConvertible & StatementColumnConvertible
{
public typealias Element = Value
public let _statement: Statement
public var _isDone = false
@usableFromInline let columnIndex: CInt
init(statement: Statement, arguments: StatementArguments? = nil, adapter: (any RowAdapter)? = nil) throws {
self._statement = statement
if let adapter {
// adapter may redefine the index of the leftmost column
columnIndex = try CInt(adapter.baseColumnIndex(atIndex: 0, layout: statement))
} else {
columnIndex = 0
}
// Assume cursor is created for immediate iteration: reset and set arguments
try statement.prepareExecution(withArguments: arguments)
}
deinit {
// Statement reset fails when sqlite3_step has previously failed.
// Just ignore reset error.
try? _statement.reset()
}
@inlinable
public func _element(sqliteStatement: SQLiteStatement) throws -> Value {
try Value.fastDecode(
fromStatement: sqliteStatement,
atUncheckedIndex: columnIndex,
context: RowDecodingContext(statement: _statement, index: Int(columnIndex)))
}
}
// Explicit non-conformance to Sendable: database cursors must be used from
// a serialized database access dispatch queue.
@available(*, unavailable)
extension FastDatabaseValueCursor: Sendable { }
/// Types that adopt both DatabaseValueConvertible and
/// StatementColumnConvertible can be efficiently initialized from
/// database values.
///
/// See DatabaseValueConvertible for more information.
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
// MARK: Fetching From Prepared Statement
/// Returns a cursor over values fetched from a prepared statement.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let statement = try db.makeStatement(sql: sql)
/// let scores = try Int.fetchCursor(statement, arguments: [lastName])
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchCursor(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> FastDatabaseValueCursor<Self>
{
try FastDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
}
/// Returns an array of values fetched from a prepared statement.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let statement = try db.makeStatement(sql: sql)
/// let scores = try Int.fetchAll(statement, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchAll(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> [Self]
{
try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from a prepared statement.
///
/// The value is decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ? LIMIT 1"
/// let statement = try db.makeStatement(sql: sql)
/// let score = try Int.fetchOne(statement, arguments: [lastName])
/// }
/// ```
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchOne(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> Self?
{
// fetchOne handles both a missing row, and one row with a NULL value.
let cursor = try FastDatabaseValueCursor<Self?>(
statement: statement,
arguments: arguments,
adapter: adapter)
return try cursor.next() ?? nil
}
}
extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hashable {
/// Returns a set of values fetched from a prepared statement.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let statement = try db.makeStatement(sql: sql)
/// let scores = try Int.fetchSet(statement, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - statement: The statement to run.
/// - arguments: Optional statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A set of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchSet(
_ statement: Statement,
arguments: StatementArguments? = nil,
adapter: (any RowAdapter)? = nil)
throws -> Set<Self>
{
try Set(fetchCursor(statement, arguments: arguments, adapter: adapter))
}
}
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
// MARK: Fetching From SQL
/// Returns a cursor over values fetched from an SQL query.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let scores = try Int.fetchCursor(db, sql: sql, arguments: [lastName])
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchCursor(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> FastDatabaseValueCursor<Self>
{
try fetchCursor(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns an array of values fetched from an SQL query.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let scores = try Int.fetchAll(db, sql: sql, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An array of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchAll(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> [Self]
{
try fetchAll(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
/// Returns a single value fetched from an SQL query.
///
/// The value is decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let score = try Int.fetchOne(db, sql: sql, arguments: [lastName])
/// }
/// ```
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchOne(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> Self?
{
try fetchOne(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
}
extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hashable {
/// Returns a set of values fetched from an SQL query.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
/// let sql = "SELECT score FROM player WHERE lastName = ?"
/// let scores = try Int.fetchSet(db, sql: sql, arguments: [lastName])
/// }
/// ```
///
/// Values are decoded from the leftmost column if the `adapter` argument
/// is nil.
///
/// - parameters:
/// - db: A database connection.
/// - sql: An SQL string.
/// - arguments: Statement arguments.
/// - adapter: Optional RowAdapter
/// - returns: A set of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchSet(
_ db: Database,
sql: String,
arguments: StatementArguments = StatementArguments(),
adapter: (any RowAdapter)? = nil)
throws -> Set<Self>
{
try fetchSet(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
}
}
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
// MARK: Fetching From FetchRequest
/// Returns a cursor over values fetched from a fetch request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try Int.fetchCursor(db, request)
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchCursor(_ db: Database, _ request: some FetchRequest)
throws -> FastDatabaseValueCursor<Self>
{
let request = try request.makePreparedRequest(db, forSingleResult: false)
return try fetchCursor(request.statement, adapter: request.adapter)
}
/// Returns an array of values fetched from a fetch request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try Int.fetchAll(db, request)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: An array of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] {
let request = try request.makePreparedRequest(db, forSingleResult: false)
return try fetchAll(request.statement, adapter: request.adapter)
}
/// Returns a single value fetched from a fetch request.
///
/// The value is decoded from the leftmost column.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName) LIMIT 1
/// """
///
/// let scores = try Int.fetchOne(db, request)
/// }
/// ```
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? {
let request = try request.makePreparedRequest(db, forSingleResult: true)
return try fetchOne(request.statement, adapter: request.adapter)
}
}
extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hashable {
/// Returns a set of values fetched from a fetch request.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .select(Column("score"))
/// .filter(Column("lastName") == lastName)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try Int.fetchAll(db, request)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameters:
/// - db: A database connection.
/// - request: A FetchRequest.
/// - returns: A set of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set<Self> {
let request = try request.makePreparedRequest(db, forSingleResult: false)
return try fetchSet(request.statement, adapter: request.adapter)
}
}
extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementColumnConvertible {
// MARK: Fetching Values
/// Returns a cursor over fetched values.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try request.fetchCursor(db)
/// while let score = try scores.next() {
/// print(score)
/// }
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// The returned cursor is valid only during the remaining execution of the
/// database access. Do not store or return the cursor for later use.
///
/// If the database is modified during the cursor iteration, the remaining
/// elements are undefined.
///
/// - parameter db: A database connection.
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchCursor(_ db: Database) throws -> FastDatabaseValueCursor<RowDecoder> {
try RowDecoder.fetchCursor(db, self)
}
/// Returns an array of fetched values.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try request.fetchAll(db)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameter db: A database connection.
/// - returns: An array of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
try RowDecoder.fetchAll(db, self)
}
/// Returns a single fetched value.
///
/// The value is decoded from the leftmost column.
///
/// The result is nil if the request returns no row, or one row with a
/// `NULL` value.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName) LIMIT 1
/// """
///
/// let score = try request.fetchOne(db)
/// }
/// ```
///
/// - parameter db: A database connection.
/// - returns: An optional value.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchOne(_ db: Database) throws -> RowDecoder? {
try RowDecoder.fetchOne(db, self)
}
}
extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementColumnConvertible & Hashable {
/// Returns a set of fetched values.
///
/// For example:
///
/// ```swift
/// try dbQueue.read { db in
/// let lastName = "O'Reilly"
///
/// // Query interface request
/// let request = Player
/// .filter(Column("lastName") == lastName)
/// .select(Column("score"), as: Int.self)
///
/// // SQL request
/// let request: SQLRequest<Int> = """
/// SELECT score FROM player WHERE lastName = \(lastName)
/// """
///
/// let scores = try request.fetchSet(db)
/// }
/// ```
///
/// Values are decoded from the leftmost column.
///
/// - parameter db: A database connection.
/// - returns: A set of values.
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
public func fetchSet(_ db: Database) throws -> Set<RowDecoder> {
try RowDecoder.fetchSet(db, self)
}
}
@@ -0,0 +1,18 @@
#if canImport(CoreGraphics)
import CoreGraphics
/// CGFloat adopts DatabaseValueConvertible
extension CGFloat: DatabaseValueConvertible {
/// Returns a REAL database value.
public var databaseValue: DatabaseValue {
Double(self).databaseValue
}
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> CGFloat? {
guard let double = Double.fromDatabaseValue(dbValue) else {
return nil
}
return CGFloat(double)
}
}
#endif
@@ -0,0 +1,86 @@
import Foundation
/// Data is convertible to and from DatabaseValue.
extension Data: DatabaseValueConvertible, StatementColumnConvertible {
public init(sqliteStatement: SQLiteStatement, index: CInt) {
if let bytes = sqlite3_column_blob(sqliteStatement, index) {
let count = Int(sqlite3_column_bytes(sqliteStatement, index))
self.init(bytes: bytes, count: count) // copy bytes
} else {
self.init()
}
}
/// Returns a BLOB database value.
public var databaseValue: DatabaseValue {
DatabaseValue(storage: .blob(self))
}
/// Returns a `Data` from the specified database value.
///
/// If the database value contains a data blob, returns it.
///
/// If the database value contains a string, returns this string converted
/// to UTF8 data.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? {
switch dbValue.storage {
case .blob(let data):
return data
case .string(let string):
// Implicit conversion from string to blob, just as SQLite does
// See <https://www.sqlite.org/c3ref/column_blob.html>
return string.data(using: .utf8)
default:
return nil
}
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
withUnsafeBytes {
sqlite3_bind_blob(sqliteStatement, index, $0.baseAddress, CInt($0.count), SQLITE_TRANSIENT)
}
}
/// Calls the given closure after binding a statement argument.
///
/// The binding is valid only during the execution of this method.
///
/// - parameter sqliteStatement: An SQLite statement.
/// - parameter index: 1-based index to statement arguments.
/// - parameter body: The closure to execute when argument is bound.
func withBinding<T>(to sqliteStatement: SQLiteStatement, at index: CInt, do body: () throws -> T) throws -> T {
try withUnsafeBytes {
let code = sqlite3_bind_blob(
sqliteStatement, index,
$0.baseAddress, CInt($0.count), nil /* SQLITE_STATIC */)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
}
}
}
// MARK: - Conversions
extension Data {
static func fastDecodeNoCopy(
fromStatement sqliteStatement: SQLiteStatement,
atUncheckedIndex index: CInt,
context: @autoclosure () -> RowDecodingContext)
throws -> Data
{
guard sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL else {
throw RowDecodingError.valueMismatch(
Data.self,
sqliteStatement: sqliteStatement,
index: index,
context: context())
}
guard let bytes = sqlite3_column_blob(sqliteStatement, index) else {
return Data()
}
let count = Int(sqlite3_column_bytes(sqliteStatement, index))
return Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: bytes), count: count, deallocator: .none)
}
}
@@ -0,0 +1,171 @@
import Foundation
/// A database value that holds date components.
public struct DatabaseDateComponents: Sendable {
/// The SQLite formats for date components.
public enum Format: String, Sendable {
/// The format "yyyy-MM-dd".
case YMD = "yyyy-MM-dd"
/// The format "yyyy-MM-dd HH:mm".
///
/// This format is lexically comparable with SQLite's CURRENT_TIMESTAMP.
case YMD_HM = "yyyy-MM-dd HH:mm"
/// The format "yyyy-MM-dd HH:mm:ss".
///
/// This format is lexically comparable with SQLite's CURRENT_TIMESTAMP.
case YMD_HMS = "yyyy-MM-dd HH:mm:ss"
/// The format "yyyy-MM-dd HH:mm:ss.SSS".
///
/// This format is lexically comparable with SQLite's CURRENT_TIMESTAMP.
case YMD_HMSS = "yyyy-MM-dd HH:mm:ss.SSS"
/// The format "HH:mm".
case HM = "HH:mm"
/// The format "HH:mm:ss".
case HMS = "HH:mm:ss"
/// The format "HH:mm:ss.SSS".
case HMSS = "HH:mm:ss.SSS"
var hasYMDComponents: Bool {
switch self {
case .YMD, .YMD_HM, .YMD_HMS, .YMD_HMSS:
return true
case .HM, .HMS, .HMSS:
return false
}
}
}
/// The date components
public let dateComponents: DateComponents
/// The database format
public let format: Format
/// Creates a DatabaseDateComponents from a DateComponents and a format.
///
/// - parameters:
/// - dateComponents: An optional DateComponents.
/// - format: The format used for storing the date components in
/// the database.
public init(_ dateComponents: DateComponents, format: Format) {
self.format = format
self.dateComponents = dateComponents
}
}
extension DatabaseDateComponents: StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
guard let cString = sqlite3_column_text(sqliteStatement, index) else {
return nil
}
let length = Int(sqlite3_column_bytes(sqliteStatement, index)) // avoid an strlen
let components = cString.withMemoryRebound(
to: CChar.self,
capacity: length + 1 /* trailing \0 */) { cString in
SQLiteDateParser().components(cString: cString, length: length)
}
guard let components else {
return nil
}
self.init(components.dateComponents, format: components.format)
}
}
extension DatabaseDateComponents: DatabaseValueConvertible {
/// Returns a TEXT database value.
public var databaseValue: DatabaseValue {
let dateString: String?
switch format {
case .YMD_HM, .YMD_HMS, .YMD_HMSS, .YMD:
let year = dateComponents.year ?? 0
let month = dateComponents.month ?? 1
let day = dateComponents.day ?? 1
dateString = String(format: "%04d-%02d-%02d", year, month, day)
default:
dateString = nil
}
let timeString: String?
switch format {
case .YMD_HM, .HM:
let hour = dateComponents.hour ?? 0
let minute = dateComponents.minute ?? 0
timeString = String(format: "%02d:%02d", hour, minute)
case .YMD_HMS, .HMS:
let hour = dateComponents.hour ?? 0
let minute = dateComponents.minute ?? 0
let second = dateComponents.second ?? 0
timeString = String(format: "%02d:%02d:%02d", hour, minute, second)
case .YMD_HMSS, .HMSS:
let hour = dateComponents.hour ?? 0
let minute = dateComponents.minute ?? 0
let second = dateComponents.second ?? 0
let nanosecond = dateComponents.nanosecond ?? 0
timeString = String(
format: "%02d:%02d:%02d.%03d",
hour, minute, second, Int(round(Double(nanosecond) / 1_000_000.0)))
default:
timeString = nil
}
return [dateString, timeString].compactMap { $0 }.joined(separator: " ").databaseValue
}
/// Creates a `DatabaseDateComponents` from the specified database value.
///
/// The supported formats are:
///
/// - `YYYY-MM-DD`
/// - `YYYY-MM-DD HH:MM`
/// - `YYYY-MM-DD HH:MM:SS`
/// - `YYYY-MM-DD HH:MM:SS.SSS`
/// - `YYYY-MM-DDTHH:MM`
/// - `YYYY-MM-DDTHH:MM:SS`
/// - `YYYY-MM-DDTHH:MM:SS.SSS`
/// - `HH:MM`
/// - `HH:MM:SS`
/// - `HH:MM:SS.SSS`
///
/// Related SQLite documentation: <https://www.sqlite.org/lang_datefunc.html>
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> DatabaseDateComponents? {
guard let string = String.fromDatabaseValue(dbValue) else {
return nil
}
return SQLiteDateParser().components(from: string)
}
}
extension DatabaseDateComponents: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let stringValue = try container.decode(String.self)
guard let decodedValue = DatabaseDateComponents.fromDatabaseValue(stringValue.databaseValue) else {
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Unable to initialise databaseDateComponent")
}
self = decodedValue
}
}
extension DatabaseDateComponents: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(String.fromDatabaseValue(databaseValue)!)
}
}
@@ -0,0 +1,47 @@
import Foundation
/// DatabaseValueConvertible is free for ReferenceConvertible types whose
/// ReferenceType is itself DatabaseValueConvertible.
///
/// class FooReference { ... }
/// struct Foo : ReferenceConvertible {
/// typealias ReferenceType = FooReference
/// }
///
/// // If the ReferenceType adopts DatabaseValueConvertible...
/// extension FooReference : DatabaseValueConvertible { ... }
///
/// // ... then the ReferenceConvertible type can freely adopt DatabaseValueConvertible:
/// extension Foo : DatabaseValueConvertible { /* empty */ }
extension DatabaseValueConvertible where Self: ReferenceConvertible, Self.ReferenceType: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
(self as! ReferenceType).databaseValue
}
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
ReferenceType.fromDatabaseValue(dbValue).flatMap { cast($0) }
}
}
extension DatabaseValueConvertible
where
Self: Decodable & ReferenceConvertible,
Self.ReferenceType: DatabaseValueConvertible
{
public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? {
// Preserve custom database decoding
return ReferenceType.fromDatabaseValue(databaseValue).flatMap { cast($0) }
}
}
extension DatabaseValueConvertible
where
Self: Encodable & ReferenceConvertible,
Self.ReferenceType: DatabaseValueConvertible
{
public var databaseValue: DatabaseValue {
// Preserve custom database encoding
return (self as! ReferenceType).databaseValue
}
}
@@ -0,0 +1,159 @@
import Foundation
#if !os(Linux)
/// NSDate is stored in the database using the format
/// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone.
extension NSDate: DatabaseValueConvertible {
/// Returns a TEXT database value that contains the date encoded as
/// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone.
public var databaseValue: DatabaseValue {
(self as Date).databaseValue
}
/// Creates an `NSDate` with the specified database value.
///
/// If the database value contains a number, that number is interpreted as a
/// timeinterval since 00:00:00 UTC on 1 January 1970.
///
/// If the database value contains a string, that string is interpreted as a
/// [SQLite date](https://sqlite.org/lang_datefunc.html) in the UTC time
/// zone. Nil is returned if the date string does not contain at least the
/// year, month and day components. Other components (minutes, etc.)
/// are set to zero if missing.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
guard let date = Date.fromDatabaseValue(dbValue) else {
return nil
}
return cast(date)
}
}
#endif
/// Date is stored in the database using the format
/// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone.
extension Date: DatabaseValueConvertible {
/// Returns a TEXT database value that contains the date encoded as
/// "yyyy-MM-dd HH:mm:ss.SSS", in the UTC time zone.
public var databaseValue: DatabaseValue {
storageDateFormatter.string(from: self).databaseValue
}
/// Creates an `Date` with the specified database value.
///
/// If the database value contains a number, that number is interpreted as a
/// timeinterval since 00:00:00 UTC on 1 January 1970.
///
/// If the database value contains a string, that string is interpreted as a
/// [SQLite date](https://sqlite.org/lang_datefunc.html) in the UTC time
/// zone. Nil is returned if the date string does not contain at least the
/// year, month and day components. Other components (minutes, etc.)
/// are set to zero if missing.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Date? {
if let databaseDateComponents = DatabaseDateComponents.fromDatabaseValue(dbValue) {
return Date(databaseDateComponents: databaseDateComponents)
}
if let timestamp = Double.fromDatabaseValue(dbValue) {
return Date(timeIntervalSince1970: timestamp)
}
return nil
}
@usableFromInline
init?(databaseDateComponents: DatabaseDateComponents) {
guard databaseDateComponents.format.hasYMDComponents else {
// Refuse to turn hours without any date information into Date:
return nil
}
guard let date = UTCCalendar.date(from: databaseDateComponents.dateComponents) else {
return nil
}
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
}
/// Creates a date from a [Julian Day](https://en.wikipedia.org/wiki/Julian_day).
public init?(julianDay: Double) {
// Conversion uses the same algorithm as SQLite: https://www.sqlite.org/src/artifact/8ec787fed4929d8c
// TODO: check for overflows one day, and return nil when computation can't complete.
let JD = Int64(julianDay * 86400000)
let Z = Int(((JD + 43200000)/86400000))
var A = Int(((Double(Z) - 1867216.25)/36524.25))
A = Z + 1 + A - (A/4)
let B = A + 1524
let C = Int(((Double(B) - 122.1)/365.25))
let D = (36525*(C&32767))/100
let E = Int((Double(B-D)/30.6001))
let X1 = Int((30.6001*Double(E)))
let day = B - D - X1
let month = E<14 ? E-1 : E-13
let year = month>2 ? C - 4716 : C - 4715
var s = Int(((JD + 43200000) % 86400000))
var second = Double(s)/1000.0
s = Int(second)
second -= Double(s)
let hour = s/3600
s -= hour*3600
let minute = s/60
second += Double(s - minute*60)
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.hour = hour
dateComponents.minute = minute
dateComponents.second = Int(second)
dateComponents.nanosecond = Int((second - Double(Int(second))) * 1.0e9)
guard let date = UTCCalendar.date(from: dateComponents) else {
return nil
}
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
}
}
extension Date: StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_INTEGER, SQLITE_FLOAT:
self.init(timeIntervalSince1970: sqlite3_column_double(sqliteStatement, index))
case SQLITE_TEXT:
guard let components = DatabaseDateComponents(sqliteStatement: sqliteStatement, index: index),
let date = Date(databaseDateComponents: components)
else {
return nil
}
self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
default:
return nil
}
}
}
/// The DatabaseDate date formatter for stored dates.
private let storageDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter
}()
// The NSCalendar for stored dates.
private let UTCCalendar: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.locale = Locale(identifier: "en_US_POSIX")
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
return calendar
}()
@@ -0,0 +1,59 @@
#if !os(Linux)
import Foundation
/// Decimal adopts DatabaseValueConvertible
extension Decimal: DatabaseValueConvertible {
/// Returns a TEXT decimal value.
public var databaseValue: DatabaseValue {
NSDecimalNumber(decimal: self)
.description(withLocale: Locale(identifier: "en_US_POSIX"))
.databaseValue
}
/// Creates an `Decimal` with the specified database value.
///
/// If the database value contains a integer or a double, returns a
/// `Decimal` initialized from this number.
///
/// If the database value contains a string, parses the string with the
/// `en_US_POSIX` locale.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
switch dbValue.storage {
case .int64(let int64):
return self.init(int64)
case .double(let double):
return self.init(double)
case let .string(string):
// Must match NSNumber.fromDatabaseValue(_:)
return self.init(string: string, locale: _posixLocale)
default:
return nil
}
}
}
/// Decimal adopts StatementColumnConvertible
extension Decimal: StatementColumnConvertible {
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_INTEGER:
self.init(sqlite3_column_int64(sqliteStatement, index))
case SQLITE_FLOAT:
self.init(sqlite3_column_double(sqliteStatement, index))
case SQLITE_TEXT:
self.init(
string: String(cString: sqlite3_column_text(sqliteStatement, index)!),
locale: _posixLocale)
default:
return nil
}
}
}
@usableFromInline
let _posixLocale = Locale(identifier: "en_US_POSIX")
#endif
@@ -0,0 +1,27 @@
#if !os(Linux)
import Foundation
/// NSData is convertible to and from DatabaseValue.
extension NSData: DatabaseValueConvertible {
/// Returns a BLOB database value.
public var databaseValue: DatabaseValue {
(self as Data).databaseValue
}
/// Returns a `NSData` from the specified database value.
///
/// If the database value contains a data blob, returns it.
///
/// If the database value contains a string, returns this string converted
/// to UTF8 data.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
guard let data = Data.fromDatabaseValue(dbValue) else {
return nil
}
return cast(data)
}
}
#endif
@@ -0,0 +1,11 @@
import Foundation
/// NSNull adopts DatabaseValueConvertible
extension NSNull: DatabaseValueConvertible {
/// Returns the NULL database value.
public var databaseValue: DatabaseValue { .null }
/// Returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? { nil }
}
@@ -0,0 +1,99 @@
#if !os(Linux)
import Foundation
private let integerRoundingBehavior = NSDecimalNumberHandler(
roundingMode: .plain,
scale: 0,
raiseOnExactness: false,
raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false)
/// NSNumber adopts DatabaseValueConvertible
extension NSNumber: DatabaseValueConvertible {
/// A database value.
///
/// If the number is an integer `NSDecimalNumber`, returns an INTEGER
/// database value.
///
/// Otherwise, returns an INTEGER or REAL database value, according to the
/// value stored in the `NSNumber`.
public var databaseValue: DatabaseValue {
// Don't lose precision: store integers that fits in Int64 as Int64
if let decimal = self as? NSDecimalNumber,
decimal == decimal.rounding(accordingToBehavior: integerRoundingBehavior), // integer
decimal.compare(NSDecimalNumber(value: Int64.max)) != .orderedDescending, // decimal <= Int64.max
decimal.compare(NSDecimalNumber(value: Int64.min)) != .orderedAscending // decimal >= Int64.min
{
return int64Value.databaseValue
}
switch String(cString: objCType) {
case "c":
return Int64(int8Value).databaseValue
case "C":
return Int64(uint8Value).databaseValue
case "s":
return Int64(int16Value).databaseValue
case "S":
return Int64(uint16Value).databaseValue
case "i":
return Int64(int32Value).databaseValue
case "I":
return Int64(uint32Value).databaseValue
case "l":
return Int64(intValue).databaseValue
case "L":
let uint = uintValue
GRDBPrecondition(
UInt64(uint) <= UInt64(Int64.max),
"could not convert \(uint) to an Int64 that can be stored in the database")
return Int64(uint).databaseValue
case "q":
return Int64(int64Value).databaseValue
case "Q":
let uint64 = uint64Value
GRDBPrecondition(
uint64 <= UInt64(Int64.max),
"could not convert \(uint64) to an Int64 that can be stored in the database")
return Int64(uint64).databaseValue
case "f":
return Double(floatValue).databaseValue
case "d":
return doubleValue.databaseValue
case "B":
return boolValue.databaseValue
case let objCType:
// Assume a GRDB bug: there is no point throwing any error.
fatalError("DatabaseValueConvertible: Unsupported NSNumber type: \(objCType)")
}
}
/// Returns a `NSNumber` from the specified database value.
///
/// If the database value is an integer or a double, returns an `NSNumber`
/// initialized from this number.
///
/// If the database value is a string, returns an `NSDecimalNumber` parsed
/// with the `en_US_POSIX` locale.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
switch dbValue.storage {
case .int64(let int64):
return self.init(value: int64)
case .double(let double):
return self.init(value: double)
case let .string(string):
// Must match Decimal.fromDatabaseValue(_:)
guard let decimal = Decimal(string: string, locale: posixLocale) else { return nil }
return NSDecimalNumber(decimal: decimal) as? Self
default:
return nil
}
}
}
private let posixLocale = Locale(identifier: "en_US_POSIX")
#endif
@@ -0,0 +1,27 @@
#if !os(Linux)
import Foundation
/// NSString adopts DatabaseValueConvertible
extension NSString: DatabaseValueConvertible {
/// Returns a TEXT database value.
public var databaseValue: DatabaseValue {
(self as String).databaseValue
}
/// Returns a `NSString` from the specified database value.
///
/// If the database value contains a string, returns it.
///
/// If the database value contains a data blob, parses this data as an
/// UTF8 string.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
guard let string = String.fromDatabaseValue(dbValue) else {
return nil
}
return self.init(string: string)
}
}
#endif
@@ -0,0 +1,227 @@
import Foundation
// inspired by: http://jordansmith.io/performant-date-parsing/
@usableFromInline
struct SQLiteDateParser {
@usableFromInline
init() { }
func components(from dateString: String) -> DatabaseDateComponents? {
dateString.withCString { cString in
components(cString: cString, length: strlen(cString))
}
}
@usableFromInline
func components(cString: UnsafePointer<CChar>, length: Int) -> DatabaseDateComponents? {
assert(strlen(cString) == length)
// "HH:MM" is the shortest valid string
guard length >= 5 else { return nil }
// "YYYY-..." -> datetime
if cString[4] == UInt8(ascii: "-") {
var components = DateComponents()
var parser = Parser(cString: cString, length: length)
guard let format = parseDatetimeFormat(parser: &parser, into: &components),
parser.length == 0
else {
return nil
}
return DatabaseDateComponents(components, format: format)
}
// "HH-:..." -> time
if cString[2] == UInt8(ascii: ":") {
var components = DateComponents()
var parser = Parser(cString: cString, length: length)
guard let format = parseTimeFormat(parser: &parser, into: &components),
parser.length == 0
else {
return nil
}
return DatabaseDateComponents(components, format: format)
}
// Invalid
return nil
}
// - YYYY-MM-DD
// - YYYY-MM-DD HH:MM
// - YYYY-MM-DD HH:MM:SS
// - YYYY-MM-DD HH:MM:SS.SSS
// - YYYY-MM-DDTHH:MM
// - YYYY-MM-DDTHH:MM:SS
// - YYYY-MM-DDTHH:MM:SS.SSS
private func parseDatetimeFormat(
parser: inout Parser,
into components: inout DateComponents)
-> DatabaseDateComponents.Format?
{
guard let year = parser.parseNNNN(),
parser.parse("-"),
let month = parser.parseNN(),
parser.parse("-"),
let day = parser.parseNN()
else { return nil }
components.year = year
components.month = month
components.day = day
if parser.length == 0 { return .YMD }
guard parser.parse(" ") || parser.parse("T")
else {
return nil
}
switch parseTimeFormat(parser: &parser, into: &components) {
case .HM: return .YMD_HM
case .HMS: return .YMD_HMS
case .HMSS: return .YMD_HMSS
default: return nil
}
}
// - HH:MM
// - HH:MM:SS
// - HH:MM:SS.SSS
private func parseTimeFormat(
parser: inout Parser,
into components: inout DateComponents)
-> DatabaseDateComponents.Format?
{
guard let hour = parser.parseNN(),
parser.parse(":"),
let minute = parser.parseNN()
else { return nil }
components.hour = hour
components.minute = minute
if parser.length == 0 || parseTimeZone(parser: &parser, into: &components) { return .HM }
guard parser.parse(":"),
let second = parser.parseNN()
else { return nil }
components.second = second
if parser.length == 0 || parseTimeZone(parser: &parser, into: &components) { return .HMS }
guard parser.parse(".") else { return nil }
// Parse one to three digits
// Rationale: https://github.com/groue/GRDB.swift/pull/362
var nanosecond = 0
guard parser.parseDigit(into: &nanosecond) else { return nil }
if parser.length == 0 || parseTimeZone(parser: &parser, into: &components) {
components.nanosecond = nanosecond * 100_000_000
return .HMSS
}
guard parser.parseDigit(into: &nanosecond) else { return nil }
if parser.length == 0 || parseTimeZone(parser: &parser, into: &components) {
components.nanosecond = nanosecond * 10_000_000
return .HMSS
}
guard parser.parseDigit(into: &nanosecond) else { return nil }
components.nanosecond = nanosecond * 1_000_000
while parser.parseDigit() != nil { }
_ = parseTimeZone(parser: &parser, into: &components)
return .HMSS
}
private func parseTimeZone(
parser: inout Parser,
into components: inout DateComponents)
-> Bool
{
if parser.parse("Z") {
components.timeZone = TimeZone(secondsFromGMT: 0)
return true
}
if parser.parse("+"),
let hour = parser.parseNN(),
parser.parse(":"),
let minute = parser.parseNN()
{
components.timeZone = TimeZone(secondsFromGMT: hour * 3600 + minute * 60)
return true
}
if parser.parse("-"),
let hour = parser.parseNN(),
parser.parse(":"),
let minute = parser.parseNN()
{
components.timeZone = TimeZone(secondsFromGMT: -(hour * 3600 + minute * 60))
return true
}
return false
}
private struct Parser {
var cString: UnsafePointer<CChar>
var length: Int
private mutating func shift() {
cString += 1
length -= 1
}
mutating func parse(_ scalar: Unicode.Scalar) -> Bool {
guard length > 0, cString[0] == UInt8(ascii: scalar) else {
return false
}
shift()
return true
}
mutating func parseDigit() -> Int? {
guard length > 0 else {
return nil
}
let char = cString[0]
let digit = char - CChar(bitPattern: UInt8(ascii: "0"))
guard digit >= 0 && digit <= 9 else {
return nil
}
shift()
return Int(digit)
}
mutating func parseDigit(into number: inout Int) -> Bool {
guard let digit = parseDigit() else {
return false
}
number = number * 10 + digit
return true
}
mutating func parseNNNN() -> Int? {
var number = 0
guard parseDigit(into: &number)
&& parseDigit(into: &number)
&& parseDigit(into: &number)
&& parseDigit(into: &number)
else {
// Don't restore self to initial state because we don't need it
return nil
}
return number
}
mutating func parseNN() -> Int? {
var number = 0
guard parseDigit(into: &number)
&& parseDigit(into: &number)
else {
// Don't restore self to initial state because we don't need it
return nil
}
return number
}
}
}
@@ -0,0 +1,22 @@
import Foundation
#if !os(Linux)
/// NSURL stores its absoluteString in the database.
extension NSURL: DatabaseValueConvertible {
/// Returns a TEXT database value containing the absolute URL.
public var databaseValue: DatabaseValue {
absoluteString?.databaseValue ?? .null
}
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
guard let string = String.fromDatabaseValue(dbValue) else {
return nil
}
return cast(URL(string: string))
}
}
#endif
/// URL stores its absoluteString in the database.
extension URL: DatabaseValueConvertible { }
@@ -0,0 +1,91 @@
import Foundation
#if !os(Linux)
/// NSUUID adopts DatabaseValueConvertible
extension NSUUID: DatabaseValueConvertible {
/// Returns a BLOB database value containing the uuid bytes.
public var databaseValue: DatabaseValue {
var uuidBytes = ContiguousArray(repeating: UInt8(0), count: 16)
return uuidBytes.withUnsafeMutableBufferPointer { buffer in
getBytes(buffer.baseAddress!)
return NSData(bytes: buffer.baseAddress, length: 16).databaseValue
}
}
/// Returns a `NSUUID` from the specified database value.
///
/// If the database value contains a string, parses this string as an uuid.
///
/// If the database value contains a data blob that contains 16 bytes,
/// returns a uuid from those bytes.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
switch dbValue.storage {
case .blob(let data) where data.count == 16:
return data.withUnsafeBytes {
self.init(uuidBytes: $0.bindMemory(to: UInt8.self).baseAddress)
}
case .string(let string):
return self.init(uuidString: string)
default:
return nil
}
}
}
#endif
/// UUID adopts DatabaseValueConvertible
extension UUID: DatabaseValueConvertible {
/// Returns a BLOB database value containing the uuid bytes.
public var databaseValue: DatabaseValue {
withUnsafeBytes(of: uuid) {
Data(bytes: $0.baseAddress!, count: $0.count).databaseValue
}
}
/// Returns a `UUID` from the specified database value.
///
/// If the database value contains a string, parses this string as an uuid.
///
/// If the database value contains a data blob that contains 16 bytes,
/// returns a uuid from those bytes.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UUID? {
switch dbValue.storage {
case .blob(let data) where data.count == 16:
return data.withUnsafeBytes {
UUID(uuid: $0.bindMemory(to: uuid_t.self).first!)
}
case .string(let string):
return UUID(uuidString: string)
default:
return nil
}
}
}
extension UUID: StatementColumnConvertible {
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
switch sqlite3_column_type(sqliteStatement, index) {
case SQLITE_TEXT:
let string = String(cString: sqlite3_column_text(sqliteStatement, index)!)
guard let uuid = UUID(uuidString: string) else {
return nil
}
self.init(uuid: uuid.uuid)
case SQLITE_BLOB:
guard sqlite3_column_bytes(sqliteStatement, index) == 16,
let blob = sqlite3_column_blob(sqliteStatement, index) else
{
return nil
}
self.init(uuid: blob.assumingMemoryBound(to: uuid_t.self).pointee)
default:
return nil
}
}
}
@@ -0,0 +1,194 @@
import Foundation
private struct DatabaseValueDecodingContainer: SingleValueDecodingContainer {
let dbValue: DatabaseValue
let codingPath: [any CodingKey]
/// Decodes a null value.
///
/// - returns: Whether the encountered value was null.
func decodeNil() -> Bool { dbValue.isNull }
/// Decodes a single value of the given type.
///
/// - parameter type: The type to decode as.
/// - returns: A value of the requested type.
/// - throws: `DecodingError.typeMismatch` if the encountered encoded value
/// cannot be converted to the requested type.
/// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null.
func decode(_ type: Bool.Type) throws -> Bool {
if let result = Bool.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Int.Type) throws -> Int {
if let result = Int.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Int8.Type) throws -> Int8 {
if let result = Int8.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Int16.Type) throws -> Int16 {
if let result = Int16.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Int32.Type) throws -> Int32 {
if let result = Int32.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Int64.Type) throws -> Int64 {
if let result = Int64.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: UInt.Type) throws -> UInt {
if let result = UInt.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: UInt8.Type) throws -> UInt8 {
if let result = UInt8.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: UInt16.Type) throws -> UInt16 {
if let result = UInt16.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: UInt32.Type) throws -> UInt32 {
if let result = UInt32.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: UInt64.Type) throws -> UInt64 {
if let result = UInt64.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Float.Type) throws -> Float {
if let result = Float.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: Double.Type) throws -> Double {
if let result = Double.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
func decode(_ type: String.Type) throws -> String {
if let result = String.fromDatabaseValue(dbValue) {
return result
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
}
/// Decodes a single value of the given type.
///
/// - parameter type: The type to decode as.
/// - returns: A value of the requested type.
/// - throws: `DecodingError.typeMismatch` if the encountered encoded value
/// cannot be converted to the requested type.
/// - throws: `DecodingError.valueNotFound` if the encountered encoded value is null.
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
if let type = T.self as? any DatabaseValueConvertible.Type {
// Prefer DatabaseValueConvertible decoding over Decodable.
// This allows custom database decoding, such as decoding Date from
// String, for example.
if let result = type.fromDatabaseValue(dbValue) {
return result as! T
} else {
throw DecodingError.dataCorruptedError(in: self, debugDescription: "value mismatch")
}
} else {
return try T(from: DatabaseValueDecoder(dbValue: dbValue, codingPath: codingPath))
}
}
}
private struct DatabaseValueDecoder: Decoder {
let dbValue: DatabaseValue
let codingPath: [any CodingKey]
var userInfo: [CodingUserInfoKey: Any] { [:] }
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
// We need to switch to JSON decoding
throw JSONRequiredError()
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
// We need to switch to JSON decoding
throw JSONRequiredError()
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
DatabaseValueDecodingContainer(dbValue: dbValue, codingPath: codingPath)
}
}
extension DatabaseValueConvertible where Self: Decodable {
public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? {
do {
return try self.init(from: DatabaseValueDecoder(dbValue: databaseValue, codingPath: []))
} catch is JSONRequiredError {
guard let data = Data.fromDatabaseValue(databaseValue) else {
return nil
}
return try? databaseJSONDecoder().decode(Self.self, from: data)
} catch {
return nil
}
}
}
extension DatabaseValueConvertible where Self: Decodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible {
public static func fromDatabaseValue(_ databaseValue: DatabaseValue) -> Self? {
// Preserve custom database decoding
return RawValue.fromDatabaseValue(databaseValue).flatMap { self.init(rawValue: $0) }
}
}
@@ -0,0 +1,155 @@
import Foundation
private struct DatabaseValueEncodingContainer: SingleValueEncodingContainer {
let encode: (DatabaseValue) -> Void
let jsonEncoder: JSONEncoder
var codingPath: [any CodingKey] { [] }
/// Encodes a null value.
///
/// - throws: `EncodingError.invalidValue` if a null value is invalid in the current context for this format.
/// - precondition: May not be called after a previous `self.encode(_:)` call.
mutating func encodeNil() throws { encode(.null) }
/// Encodes a single value of the given type.
///
/// - parameter value: The value to encode.
/// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format.
/// - precondition: May not be called after a previous `self.encode(_:)` call.
mutating func encode(_ value: Bool) throws { encode(value.databaseValue) }
mutating func encode(_ value: Int) throws { encode(value.databaseValue) }
mutating func encode(_ value: Int8) throws { encode(value.databaseValue) }
mutating func encode(_ value: Int16) throws { encode(value.databaseValue) }
mutating func encode(_ value: Int32) throws { encode(value.databaseValue) }
mutating func encode(_ value: Int64) throws { encode(value.databaseValue) }
mutating func encode(_ value: UInt) throws { encode(value.databaseValue) }
mutating func encode(_ value: UInt8) throws { encode(value.databaseValue) }
mutating func encode(_ value: UInt16) throws { encode(value.databaseValue) }
mutating func encode(_ value: UInt32) throws { encode(value.databaseValue) }
mutating func encode(_ value: UInt64) throws { encode(value.databaseValue) }
mutating func encode(_ value: Float) throws { encode(value.databaseValue) }
mutating func encode(_ value: Double) throws { encode(value.databaseValue) }
mutating func encode(_ value: String) throws { encode(value.databaseValue) }
/// Encodes a single value of the given type.
///
/// - parameter value: The value to encode.
/// - throws: `EncodingError.invalidValue` if the given value is invalid in the current context for this format.
/// - precondition: May not be called after a previous `self.encode(_:)` call.
mutating func encode<T>(_ value: T) throws where T: Encodable {
if let dbValueConvertible = value as? any DatabaseValueConvertible {
// Prefer DatabaseValueConvertible encoding over Decodable.
// This allows us to encode Date as String, for example.
encode(dbValueConvertible.databaseValue)
} else {
try DatabaseValueEncoder(jsonEncoder: jsonEncoder, encode: encode).encode(value)
}
}
}
private class DatabaseValueEncoder: Encoder {
let encode: (DatabaseValue) -> Void
let jsonEncoder: JSONEncoder
var requiresJSON = false
init(
jsonEncoder: JSONEncoder,
encode: @escaping (DatabaseValue) -> Void
) {
self.jsonEncoder = jsonEncoder
self.encode = encode
}
/// The path of coding keys taken to get to this point in encoding.
/// A `nil` value indicates an unkeyed container.
var codingPath: [any CodingKey] { [] }
/// Any contextual information set by the user for encoding.
var userInfo: [CodingUserInfoKey: Any] = [:]
/// Returns an encoding container appropriate for holding multiple values keyed by the given key type.
///
/// - parameter type: The key type to use for the container.
/// - returns: A new keyed encoding container.
/// - precondition: May not be called after a prior `self.unkeyedContainer()` call.
/// - precondition: May not be called after a value has been encoded through
/// a previous `self.singleValueContainer()` call.
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
// We need to perform JSON encoding. Unfortunately we can't access the
// inner container of Foundation's JSONEncoder. At this point we must
// throw an error so that the caller can retry encoding from scratch.
// Unfortunately (bis), we can't throw right from here, so let's
// return a JSONRequiredEncoder that will throw as soon as possible.
requiresJSON = true
let container = JSONRequiredEncoder.KeyedContainer<Key>(codingPath: codingPath)
return KeyedEncodingContainer(container)
}
/// Returns an encoding container appropriate for holding multiple unkeyed values.
///
/// - returns: A new empty unkeyed container.
/// - precondition: May not be called after a prior `self.container(keyedBy:)` call.
/// - precondition: May not be called after a value has been encoded through
/// a previous `self.singleValueContainer()` call.
func unkeyedContainer() -> UnkeyedEncodingContainer {
// We need to perform JSON encoding. Unfortunately we can't access the
// inner container of Foundation's JSONEncoder. At this point we must
// throw an error so that the caller can retry encoding from scratch.
// Unfortunately (bis), we can't throw right from here, so let's
// return a JSONRequiredEncoder that will throw as soon as possible.
requiresJSON = true
return JSONRequiredEncoder(codingPath: codingPath)
}
/// Returns an encoding container appropriate for holding a single primitive value.
///
/// - returns: A new empty single value container.
/// - precondition: May not be called after a prior `self.container(keyedBy:)` call.
/// - precondition: May not be called after a prior `self.unkeyedContainer()` call.
/// - precondition: May not be called after a value has been encoded through
/// a previous `self.singleValueContainer()` call.
func singleValueContainer() -> SingleValueEncodingContainer {
DatabaseValueEncodingContainer(encode: encode, jsonEncoder: jsonEncoder)
}
func encode<T: Encodable>(_ value: T) throws {
do {
try value.encode(to: self)
if requiresJSON {
// Here we handle empty arrays and dictionaries.
throw JSONRequiredError()
}
} catch is JSONRequiredError {
let jsonData = try jsonEncoder.encode(value)
// Store JSON String in the database for easier debugging and
// database inspection. Thanks to SQLite weak typing, we won't
// have any trouble decoding this string into data when we
// eventually perform JSON decoding.
// TODO: possible optimization: avoid this conversion to string,
// and store raw data bytes as an SQLite string
let jsonString = String(data: jsonData, encoding: .utf8)!
try jsonString.encode(to: self)
}
}
}
extension DatabaseValueConvertible where Self: Encodable {
public var databaseValue: DatabaseValue {
var dbValue: DatabaseValue! = nil
try! DatabaseValueEncoder(
jsonEncoder: Self.databaseJSONEncoder(),
encode: { dbValue = $0 }
)
.encode(self)
return dbValue
}
}
extension DatabaseValueConvertible where Self: Encodable & RawRepresentable, Self.RawValue: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
// Preserve custom database encoding
return rawValue.databaseValue
}
}
@@ -0,0 +1,73 @@
extension SQLSelectable where Self: RawRepresentable, Self.RawValue: SQLSelectable {
public var sqlSelection: SQLSelection {
rawValue.sqlSelection
}
}
extension SQLOrderingTerm where Self: RawRepresentable, Self.RawValue: SQLOrderingTerm {
public var sqlOrdering: SQLOrdering {
rawValue.sqlOrdering
}
}
extension SQLSpecificExpressible where Self: RawRepresentable, Self.RawValue: SQLSpecificExpressible { }
extension SQLExpressible where Self: RawRepresentable, Self.RawValue: SQLExpressible {
/// Returns the raw value as an SQL expression.
public var sqlExpression: SQLExpression {
rawValue.sqlExpression
}
}
extension StatementBinding where Self: RawRepresentable, Self.RawValue: StatementBinding {
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
rawValue.bind(to: sqliteStatement, at: index)
}
}
/// `StatementColumnConvertible` is free for `RawRepresentable` types whose raw
/// value is itself `StatementColumnConvertible`.
///
/// // If the RawValue adopts StatementColumnConvertible...
/// enum Color : Int {
/// case red
/// case white
/// case rose
/// }
///
/// // ... then the RawRepresentable type can freely
/// // adopt StatementColumnConvertible:
/// extension Color: StatementColumnConvertible { }
extension StatementColumnConvertible where Self: RawRepresentable, Self.RawValue: StatementColumnConvertible {
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
guard let rawValue = RawValue(sqliteStatement: sqliteStatement, index: index) else {
return nil
}
self.init(rawValue: rawValue)
}
}
/// `DatabaseValueConvertible` is free for `RawRepresentable` types whose raw
/// value is itself `DatabaseValueConvertible`.
///
/// // If the RawValue adopts DatabaseValueConvertible...
/// enum Color : Int {
/// case red
/// case white
/// case rose
/// }
///
/// // ... then the RawRepresentable type can freely
/// // adopt DatabaseValueConvertible:
/// extension Color: DatabaseValueConvertible { }
extension DatabaseValueConvertible where Self: RawRepresentable, Self.RawValue: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
rawValue.databaseValue
}
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
RawValue.fromDatabaseValue(dbValue).flatMap { self.init(rawValue: $0) }
}
}
@@ -0,0 +1,97 @@
struct JSONRequiredError: Error { }
/// The encoder that always ends up with a JSONRequiredError
struct JSONRequiredEncoder: Encoder {
var codingPath: [any CodingKey]
var userInfo: [CodingUserInfoKey: Any] { Record.databaseEncodingUserInfo }
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
let container = KeyedContainer<Key>(codingPath: codingPath)
return KeyedEncodingContainer(container)
}
func unkeyedContainer() -> UnkeyedEncodingContainer { self }
func singleValueContainer() -> SingleValueEncodingContainer { self }
struct KeyedContainer<KeyType: CodingKey>: KeyedEncodingContainerProtocol {
var codingPath: [any CodingKey]
var userInfo: [CodingUserInfoKey: Any] { Record.databaseEncodingUserInfo }
// swiftlint:disable comma
func encodeNil(forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Bool, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Int, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Int8, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Int16, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Int32, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Int64, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: UInt, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: UInt8, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: UInt16, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: UInt32, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: UInt64, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Float, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: Double, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode(_ value: String, forKey key: KeyType) throws { throw JSONRequiredError() }
func encode<T>(_ value: T, forKey key: KeyType) throws where T: Encodable { throw JSONRequiredError() }
// swiftlint:enable comma
func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type,
forKey key: KeyType)
-> KeyedEncodingContainer<NestedKey>
where NestedKey: CodingKey
{
let container = KeyedContainer<NestedKey>(codingPath: codingPath + [key])
return KeyedEncodingContainer(container)
}
func nestedUnkeyedContainer(forKey key: KeyType) -> UnkeyedEncodingContainer {
JSONRequiredEncoder(codingPath: codingPath)
}
func superEncoder() -> Encoder {
JSONRequiredEncoder(codingPath: codingPath)
}
func superEncoder(forKey key: KeyType) -> Encoder {
JSONRequiredEncoder(codingPath: codingPath)
}
}
}
extension JSONRequiredEncoder: SingleValueEncodingContainer {
func encodeNil() throws { throw JSONRequiredError() }
func encode(_ value: Bool ) throws { throw JSONRequiredError() }
func encode(_ value: Int ) throws { throw JSONRequiredError() }
func encode(_ value: Int8 ) throws { throw JSONRequiredError() }
func encode(_ value: Int16 ) throws { throw JSONRequiredError() }
func encode(_ value: Int32 ) throws { throw JSONRequiredError() }
func encode(_ value: Int64 ) throws { throw JSONRequiredError() }
func encode(_ value: UInt ) throws { throw JSONRequiredError() }
func encode(_ value: UInt8 ) throws { throw JSONRequiredError() }
func encode(_ value: UInt16) throws { throw JSONRequiredError() }
func encode(_ value: UInt32) throws { throw JSONRequiredError() }
func encode(_ value: UInt64) throws { throw JSONRequiredError() }
func encode(_ value: Float ) throws { throw JSONRequiredError() }
func encode(_ value: Double) throws { throw JSONRequiredError() }
func encode(_ value: String) throws { throw JSONRequiredError() }
func encode<T>(_ value: T) throws where T: Encodable { throw JSONRequiredError() }
}
extension JSONRequiredEncoder: UnkeyedEncodingContainer {
var count: Int { 0 }
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type)
-> KeyedEncodingContainer<NestedKey>
where NestedKey: CodingKey
{
let container = KeyedContainer<NestedKey>(codingPath: codingPath)
return KeyedEncodingContainer(container)
}
mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { self }
mutating func superEncoder() -> Encoder { self }
}
@@ -0,0 +1,97 @@
extension Optional: StatementBinding where Wrapped: StatementBinding {
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
switch self {
case .none:
return sqlite3_bind_null(sqliteStatement, index)
case let .some(value):
return value.bind(to: sqliteStatement, at: index)
}
}
}
extension Optional: SQLExpressible where Wrapped: SQLExpressible {
public var sqlExpression: SQLExpression {
switch self {
case .none:
return .null
case let .some(value):
return value.sqlExpression
}
}
}
extension Optional: SQLOrderingTerm where Wrapped: SQLOrderingTerm {
public var sqlOrdering: SQLOrdering {
switch self {
case .none:
return .expression(.null)
case let .some(value):
return value.sqlOrdering
}
}
}
extension Optional: SQLSelectable where Wrapped: SQLSelectable {
public var sqlSelection: SQLSelection {
switch self {
case .none:
return .expression(.null)
case let .some(value):
return value.sqlSelection
}
}
}
extension Optional: SQLSpecificExpressible where Wrapped: SQLSpecificExpressible { }
extension Optional: DatabaseValueConvertible where Wrapped: DatabaseValueConvertible {
public var databaseValue: DatabaseValue {
switch self {
case .none:
return .null
case let .some(value):
return value.databaseValue
}
}
public static func fromMissingColumn() -> Self? {
.some(.none) // success
}
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
if let value = Wrapped.fromDatabaseValue(dbValue) {
// Valid value
return value
} else if dbValue.isNull {
// NULL
return .some(.none)
} else {
// Invalid value
return .none
}
}
}
extension Optional: StatementColumnConvertible where Wrapped: StatementColumnConvertible {
@inline(__always)
@inlinable
public static func fromStatement(_ sqliteStatement: SQLiteStatement, atUncheckedIndex index: CInt) -> Self? {
if let value = Wrapped.fromStatement(sqliteStatement, atUncheckedIndex: index) {
// Valid value
return value
} else if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
// NULL
return .some(.none)
} else {
// Invalid value
return .none
}
}
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
guard let value = Wrapped(sqliteStatement: sqliteStatement, index: index) else {
return nil
}
self = .some(value)
}
}
@@ -0,0 +1,927 @@
// MARK: - Value Types
/// Bool adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Bool: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
self = sqlite3_column_int64(sqliteStatement, index) != 0
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
(self ? 1 : 0).databaseValue
}
/// Returns a `Bool` from the specified database value.
///
/// If the database value contains an integer or a double, returns whether
/// this number is zero.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Bool? {
// IMPLEMENTATION NOTE
//
// https://www.sqlite.org/lang_expr.html#booleanexpr
//
// > # Boolean Expressions
// >
// > The SQL language features several contexts where an expression is
// > evaluated and the result converted to a boolean (true or false)
// > value. These contexts are:
// >
// > - the WHERE clause of a SELECT, UPDATE or DELETE statement,
// > - the ON or USING clause of a join in a SELECT statement,
// > - the HAVING clause of a SELECT statement,
// > - the WHEN clause of an SQL trigger, and
// > - the WHEN clause or clauses of some CASE expressions.
// >
// > To convert the results of an SQL expression to a boolean value,
// > SQLite first casts the result to a NUMERIC value in the same way as
// > a CAST expression. A numeric zero value (integer value 0 or real
// > value 0.0) is considered to be false. A NULL value is still NULL.
// > All other values are considered true.
// >
// > For example, the values NULL, 0.0, 0, 'english' and '0' are all
// > considered to be false. Values 1, 1.0, 0.1, -0.1 and '1english' are
// > considered to be true.
//
// OK so we have to support boolean for all storage classes?
// Actually we won't, because of the SQLite boolean interpretation of
// strings:
//
// The doc says that "english" should be false, and "1english" should
// be true. I guess "-1english" and "0.1english" should be true also.
// And... what about "0.0e10english"?
//
// Ideally, we'd ask SQLite to perform the conversion itself, and return
// its own boolean interpretation of the string. Unfortunately, it looks
// like it is not so easy...
//
// So we could take a short route, and assume all strings are false,
// since most strings are falsey for SQLite.
//
// Considering all strings falsey is unfortunately very
// counter-intuitive. This is not the correct way to tackle the boolean
// problem.
//
// Instead, let's use the fact that the BOOLEAN typename has Numeric
// affinity (https://www.sqlite.org/datatype3.html), and that the doc
// says:
//
// > SQLite does not have a separate Boolean storage class. Instead,
// > Boolean values are stored as integers 0 (false) and 1 (true).
//
// So we extract bools from Integer and Real only. Integer because it is
// the natural boolean storage class, and Real because Numeric affinity
// store big numbers as Real.
switch dbValue.storage {
case .int64(let int64):
return (int64 != 0)
case .double(let double):
return (double != 0.0)
default:
return nil
}
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, self ? 1 : 0)
}
}
/// Int adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Int: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = Int(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `Int` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int? {
Int64.fromDatabaseValue(dbValue).flatMap { Int(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// Int8 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Int8: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = Int8(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `Int8` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int8? {
Int64.fromDatabaseValue(dbValue).flatMap { Int8(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// Int16 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Int16: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = Int16(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `Int16` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int16? {
Int64.fromDatabaseValue(dbValue).flatMap { Int16(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// Int32 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Int32: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = Int32(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `Int32` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int32? {
Int64.fromDatabaseValue(dbValue).flatMap { Int32(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// Int64 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Int64: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
self = sqlite3_column_int64(sqliteStatement, index)
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
DatabaseValue(storage: .int64(self))
}
/// Returns a `Int64` from the specified database value.
///
/// If the database value contains a integer, returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int64? {
switch dbValue.storage {
case .int64(let int64):
return int64
case .double(let double):
guard double >= Double(Int64.min) else { return nil }
guard double < Double(Int64.max) else { return nil }
return Int64(double)
default:
return nil
}
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, self)
}
}
/// UInt adopts DatabaseValueConvertible and StatementColumnConvertible.
extension UInt: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = UInt(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `UInt` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt? {
Int64.fromDatabaseValue(dbValue).flatMap { UInt(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// UInt8 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension UInt8: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = UInt8(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `UInt8` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt8? {
Int64.fromDatabaseValue(dbValue).flatMap { UInt8(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// UInt16 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension UInt16: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = UInt16(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `UInt16` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt16? {
Int64.fromDatabaseValue(dbValue).flatMap { UInt16(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// UInt32 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension UInt32: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = UInt32(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `UInt32` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt32? {
Int64.fromDatabaseValue(dbValue).flatMap { UInt32(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// UInt64 adopts DatabaseValueConvertible and StatementColumnConvertible.
extension UInt64: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
@inline(__always)
@inlinable
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let v = UInt64(exactly: int64) else { return nil }
self = v
}
/// Returns an INTEGER database value.
public var databaseValue: DatabaseValue {
Int64(self).databaseValue
}
/// Returns a `UInt64` from the specified database value.
///
/// If the database value contains a integer representable in this type,
/// returns this integer.
///
/// If the database value contains a double representable in this type after
/// rounding toward zero, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt64? {
Int64.fromDatabaseValue(dbValue).flatMap { UInt64(exactly: $0) }
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
}
}
/// Double adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Double: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
self = sqlite3_column_double(sqliteStatement, index)
}
/// Returns a REAL database value.
public var databaseValue: DatabaseValue {
DatabaseValue(storage: .double(self))
}
/// Returns a `Double` from the specified database value.
///
/// If the database value contains a integer, returns the conversion.
///
/// If the database value contains a double, returns this double.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Double? {
switch dbValue.storage {
case .int64(let int64):
return Double(int64)
case .double(let double):
return double
default:
return nil
}
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_double(sqliteStatement, index, self)
}
}
/// Float adopts DatabaseValueConvertible and StatementColumnConvertible.
extension Float: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
self = Float(sqlite3_column_double(sqliteStatement, index))
}
/// Returns a REAL database value.
public var databaseValue: DatabaseValue {
Double(self).databaseValue
}
/// Returns a `Float` from the specified database value.
///
/// If the database value contains a integer, returns the conversion.
///
/// If the database value contains a double, returns the conversion.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Float? {
switch dbValue.storage {
case .int64(let int64):
return Float(int64)
case .double(let double):
return Float(double)
default:
return nil
}
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_double(sqliteStatement, index, Double(self))
}
}
/// String adopts DatabaseValueConvertible and StatementColumnConvertible.
extension String: DatabaseValueConvertible, StatementColumnConvertible {
/// Returns a value initialized from a raw SQLite statement pointer.
///
/// - parameters:
/// - sqliteStatement: A pointer to an SQLite statement.
/// - index: The column index.
public init(sqliteStatement: SQLiteStatement, index: CInt) {
self = String(cString: sqlite3_column_text(sqliteStatement, index)!)
}
/// Returns a TEXT database value.
public var databaseValue: DatabaseValue {
DatabaseValue(storage: .string(self))
}
/// Returns a `String` from the specified database value.
///
/// If the database value contains a string, returns it.
///
/// If the database value contains a data blob, parses this data as an
/// UTF8 string.
///
/// Otherwise, returns nil.
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> String? {
switch dbValue.storage {
case .blob(let data):
// Implicit conversion from blob to string, just as SQLite does
// See <https://www.sqlite.org/c3ref/column_blob.html>
return String(data: data, encoding: .utf8)
case .string(let string):
return string
default:
return nil
}
}
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
sqlite3_bind_text(sqliteStatement, index, self, -1, SQLITE_TRANSIENT)
}
/// Calls the given closure after binding a statement argument.
///
/// The binding is valid only during the execution of this method.
///
/// - parameter sqliteStatement: An SQLite statement.
/// - parameter index: 1-based index to statement arguments.
/// - parameter body: The closure to execute when argument is bound.
func withBinding<T>(to sqliteStatement: SQLiteStatement, at index: CInt, do body: () throws -> T) throws -> T {
try withCString {
let code = sqlite3_bind_text(sqliteStatement, index, $0, -1, nil /* SQLITE_STATIC */)
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
return try body()
}
}
}
// MARK: - SQL Functions
extension DatabaseFunction {
/// An SQL function that calls the Foundation
/// `String.capitalized` property.
///
/// `NULL` is returned for non-strings values.
///
/// This function is automatically added by GRDB to your database
/// connections. It is the function used by the query interface's
/// ``SQLSpecificExpressible/capitalized``:
///
/// ```swift
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.capitalized)
/// let names = try String.fetchAll(dbQueue, request) // [String]
/// ```
public static let capitalize =
DatabaseFunction("swiftCapitalizedString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
}
return string.capitalized
}
/// An SQL function that calls the Swift
/// `String.lowercased()` method.
///
/// `NULL` is returned for non-strings values.
///
/// This function is automatically added by GRDB to your database
/// connections. It is the function used by the query interface's
/// ``SQLSpecificExpressible/lowercased``:
///
/// ```swift
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.lowercased)
/// let names = try String.fetchAll(dbQueue, request) // [String]
/// ```
public static let lowercase =
DatabaseFunction("swiftLowercaseString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
}
return string.lowercased()
}
/// An SQL function that calls the Swift
/// `String.uppercased()` method.
///
/// `NULL` is returned for non-strings values.
///
/// This function is automatically added by GRDB to your database
/// connections. It is the function used by the query interface's
/// ``SQLSpecificExpressible/uppercased``:
///
/// ```swift
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.uppercased)
/// let names = try String.fetchAll(dbQueue, request) // [String]
/// ```
public static let uppercase =
DatabaseFunction("swiftUppercaseString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
}
return string.uppercased()
}
/// An SQL function that calls the Foundation
/// `String.localizedCapitalized` property.
///
/// `NULL` is returned for non-strings values.
///
/// This function is automatically added by GRDB to your database
/// connections. It is the function used by the query interface's
/// ``SQLSpecificExpressible/localizedCapitalized``:
///
/// ```swift
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedCapitalized)
/// let names = try String.fetchAll(dbQueue, request) // [String]
/// ```
public static let localizedCapitalize =
DatabaseFunction("swiftLocalizedCapitalizedString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
}
return string.localizedCapitalized
}
/// An SQL function that calls the Foundation
/// `String.localizedLowercase` property.
///
/// `NULL` is returned for non-strings values.
///
/// This function is automatically added by GRDB to your database
/// connections. It is the function used by the query interface's
/// ``SQLSpecificExpressible/localizedLowercased``:
///
/// ```swift
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedLowercase)
/// let names = try String.fetchAll(dbQueue, request) // [String]
/// ```
public static let localizedLowercase =
DatabaseFunction("swiftLocalizedLowercaseString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
}
return string.localizedLowercase
}
/// An SQL function that calls the Foundation
/// `String.localizedUppercase` property.
///
/// `NULL` is returned for non-strings values.
///
/// This function is automatically added by GRDB to your database
/// connections. It is the function used by the query interface's
/// ``SQLSpecificExpressible/localizedUppercased``:
///
/// ```swift
/// let nameColumn = Column("name")
/// let request = Player.select(nameColumn.localizedUppercase)
/// let names = try String.fetchAll(dbQueue, request) // [String]
/// ```
public static let localizedUppercase =
DatabaseFunction("swiftLocalizedUppercaseString", argumentCount: 1, pure: true) { dbValues in
guard let string = String.fromDatabaseValue(dbValues[0]) else {
return nil
}
return string.localizedUppercase
}
}
// MARK: - SQLite Collations
extension DatabaseCollation {
// Here we define a set of predefined collations.
//
// We should avoid renaming those collations, because database created with
// earlier versions of the library may have used those collations in the
// definition of tables. A renaming would prevent SQLite to find the
// collation.
//
// Yet we're not absolutely stuck: we could register support for obsolete
// collation names with sqlite3_collation_needed().
// See https://www.sqlite.org/capi3ref.html#sqlite3_collation_needed
/// A collation that compares strings according to the built-in `==` and
/// `<=` operators of the Swift String.
///
/// This collation is automatically added by GRDB to your database
/// connections.
///
/// You can use the collation when creating database tables:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text).collate(.unicodeCompare)
/// }
/// ```
///
/// Embed the collation name in your raw SQL queries:
///
/// ```swift
/// let collationName = DatabaseCollation.unicodeCompare.name
/// dbQueue.execute(sql: """
/// CREATE TABLE player (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
/// ```
public static let unicodeCompare =
DatabaseCollation("swiftCompare") { (lhs, rhs) in
(lhs < rhs) ? .orderedAscending : ((lhs == rhs) ? .orderedSame : .orderedDescending)
}
/// A collation that compares strings according to the Foundation
/// `String.caseInsensitiveCompare(_:)` method.
///
/// This collation is automatically added by GRDB to your database
/// connections.
///
/// You can use the collation when creating database tables:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text).collate(.caseInsensitiveCompare)
/// }
/// ```
///
/// Embed the collation name in your raw SQL queries:
///
/// ```swift
/// let collationName = DatabaseCollation.caseInsensitiveCompare.name
/// dbQueue.execute(sql: """
/// CREATE TABLE player (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
/// ```
public static let caseInsensitiveCompare =
DatabaseCollation("swiftCaseInsensitiveCompare") { (lhs, rhs) in
lhs.caseInsensitiveCompare(rhs)
}
/// A collation that compares strings according to the Foundation
/// `String.localizedCaseInsensitiveCompare(_:)` method.
///
/// This collation is automatically added by GRDB to your database
/// connections.
///
/// You can use the collation when creating database tables:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text).collate(.localizedCaseInsensitiveCompare)
/// }
/// ```
///
/// Embed the collation name in your raw SQL queries:
///
/// ```swift
/// let collationName = DatabaseCollation.localizedCaseInsensitiveCompare.name
/// dbQueue.execute(sql: """
/// CREATE TABLE player (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
/// ```
public static let localizedCaseInsensitiveCompare =
DatabaseCollation("swiftLocalizedCaseInsensitiveCompare") { (lhs, rhs) in
lhs.localizedCaseInsensitiveCompare(rhs)
}
/// A collation that compares strings according to the Foundation
/// `String.localizedCompare(_:)` method.
///
/// This collation is automatically added by GRDB to your database
/// connections.
///
/// You can use the collation when creating database tables:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text).collate(.localizedCompare)
/// }
/// ```
///
/// Embed the collation name in your raw SQL queries:
///
/// ```swift
/// let collationName = DatabaseCollation.localizedCompare.name
/// dbQueue.execute(sql: """
/// CREATE TABLE player (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
/// ```
public static let localizedCompare =
DatabaseCollation("swiftLocalizedCompare") { (lhs, rhs) in
lhs.localizedCompare(rhs)
}
/// A collation that compares strings according to the Foundation
/// `String.localizedStandardCompare(_:)` method.
///
/// This collation is automatically added by GRDB to your database
/// connections.
///
/// You can use the collation when creating database tables:
///
/// ```swift
/// try db.create(table: "player") { t in
/// t.column("name", .text).collate(.localizedStandardCompare)
/// }
/// ```
///
/// Embed the collation name in your raw SQL queries:
///
/// ```swift
/// let collationName = DatabaseCollation.localizedStandardCompare.name
/// dbQueue.execute(sql: """
/// CREATE TABLE player (
/// name TEXT COLLATE \(collationName)
/// )
/// """)
/// ```
public static let localizedStandardCompare =
DatabaseCollation("swiftLocalizedStandardCompare") { (lhs, rhs) in
lhs.localizedStandardCompare(rhs)
}
}
@@ -0,0 +1,65 @@
import Foundation
/// A type that provides the moment of a transaction.
///
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
///
/// ## Topics
///
/// ### Built-in Clocks
///
/// - ``DefaultTransactionClock``
/// - ``CustomTransactionClock``
public protocol TransactionClock {
/// Returns the date of the current transaction.
///
/// This function is called whenever a transaction starts - precisely
/// speaking, whenever the database connection leaves the auto-commit mode.
///
/// It is also called when the ``Database/transactionDate`` property is
/// called, and the database connection is not in a transaction.
///
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/get_autocommit.html>
func now(_ db: Database) throws -> Date
}
extension TransactionClock where Self == DefaultTransactionClock {
/// Returns the default clock.
public static var `default`: Self { DefaultTransactionClock() }
}
extension TransactionClock where Self == CustomTransactionClock {
/// Returns a custom clock.
///
/// The provided closure is called whenever a transaction starts - precisely
/// speaking, whenever the database connection leaves the auto-commit mode.
///
/// It is also called when the ``Database/transactionDate`` property is
/// called, and the database connection is not in a transaction.
public static func custom(_ now: @escaping (Database) throws -> Date) -> Self {
CustomTransactionClock(now)
}
}
/// The default transaction clock.
public struct DefaultTransactionClock: TransactionClock {
/// Returns the start date of the current transaction.
public func now(_ db: Database) throws -> Date {
// An opportunity to fetch transaction time from the database when
// SQLite supports the feature.
Date()
}
}
/// A custom transaction clock.
public struct CustomTransactionClock: TransactionClock {
let _now: (Database) throws -> Date
public init(_ now: @escaping (Database) throws -> Date) {
self._now = now
}
public func now(_ db: Database) throws -> Date {
try _now(db)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
// swiftlint:disable:next line_length
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
/// An instance of WALSnapshot records the state of a WAL mode database for some
/// specific point in history.
///
/// We use `WALSnapshot` to help `ValueObservation` check for changes
/// that would happen between the initial fetch, and the start of the
/// actual observation. This class has no other purpose, and is not intended to
/// become public.
///
/// It does not work with SQLCipher, because SQLCipher does not support
/// `SQLITE_ENABLE_SNAPSHOT` correctly: we have linker errors.
/// See <https://github.com/ericsink/SQLitePCL.raw/issues/452>.
///
/// With custom SQLite builds, it only works if `SQLITE_ENABLE_SNAPSHOT`
/// is defined.
///
/// With system SQLite, it can only work when the SDK exposes the C apis and
/// their availability, which means XCode 14 (identified with Swift 5.7).
///
/// Yes, this is an awfully complex logic.
///
/// See <https://www.sqlite.org/c3ref/snapshot.html>.
final class WALSnapshot: @unchecked Sendable {
// @unchecked because sqlite3_snapshot has no threading requirements.
// <https://www.sqlite.org/c3ref/snapshot.html>
let sqliteSnapshot: UnsafeMutablePointer<sqlite3_snapshot>
init(_ db: Database) throws {
var sqliteSnapshot: UnsafeMutablePointer<sqlite3_snapshot>?
let code = withUnsafeMutablePointer(to: &sqliteSnapshot) {
return sqlite3_snapshot_get(db.sqliteConnection, "main", $0)
}
guard code == SQLITE_OK else {
// <https://www.sqlite.org/c3ref/snapshot_get.html>
//
// > The following must be true for sqlite3_snapshot_get() to succeed. [...]
// >
// > 1. The database handle must not be in autocommit mode.
// > 2. Schema S of database connection D must be a WAL
// > mode database.
// > 3. There must not be a write transaction open on schema S
// > of database connection D.
// > 4. One or more transactions must have been written to the
// > current wal file since it was created on disk (by any
// > connection). This means that a snapshot cannot be taken
// > on a wal mode database with no wal file immediately
// > after it is first opened. At least one transaction must
// > be written to it first.
// Test condition 1:
if sqlite3_get_autocommit(db.sqliteConnection) != 0 {
throw DatabaseError(resultCode: code, message: """
Can't create snapshot because database is in autocommit mode.
""")
}
// Test condition 2:
if let journalMode = try? String.fetchOne(db, sql: "PRAGMA journal_mode"),
journalMode != "wal"
{
throw DatabaseError(resultCode: code, message: """
Can't create snapshot because database is not in WAL mode.
""")
}
// Condition 3 can't happen because GRDB only calls this
// initializer from read transactions.
//
// Hence it is condition 4 that is false:
throw DatabaseError(resultCode: code, message: """
Can't create snapshot from a missing or empty wal file.
""")
}
guard let sqliteSnapshot else {
throw DatabaseError(resultCode: .SQLITE_INTERNAL) // WTF SQLite?
}
self.sqliteSnapshot = sqliteSnapshot
}
deinit {
sqlite3_snapshot_free(sqliteSnapshot)
}
/// Compares two WAL snapshots.
///
/// `a.compare(b) < 0` iff a is older than b.
///
/// See <https://www.sqlite.org/c3ref/snapshot_cmp.html>.
func compare(_ other: WALSnapshot) -> CInt {
sqlite3_snapshot_cmp(sqliteSnapshot, other.sqliteSnapshot)
}
}
#endif
@@ -0,0 +1,94 @@
// swiftlint:disable:next line_length
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
/// A long-live read-only WAL transaction.
///
/// `WALSnapshotTransaction` **takes ownership** of its reader
/// `SerializedDatabase` (TODO: make it a move-only type eventually).
class WALSnapshotTransaction {
private let reader: SerializedDatabase
private let release: (_ isInsideTransaction: Bool) -> Void
/// The state of the database at the beginning of the transaction.
let walSnapshot: WALSnapshot
/// Creates a long-live WAL transaction on a read-only connection.
///
/// The `release` closure is always called. It is called when the
/// `WALSnapshotTransaction` is deallocated, or if the initializer
/// throws.
///
/// In normal operations, the argument to `release` is always false,
/// meaning that the connection is no longer in a transaction. If true,
/// the connection has been left inside a transaction, due to
/// some error.
///
/// Usage:
///
/// ```swift
/// let transaction = WALSnapshotTransaction(
/// reader: reader,
/// release: { isInsideTransaction in
/// ...
/// })
/// ```
///
/// - parameter reader: A read-only database connection.
/// - parameter release: A closure to call when the read-only connection
/// is no longer used.
init(
onReader reader: SerializedDatabase,
release: @escaping (_ isInsideTransaction: Bool) -> Void)
throws
{
assert(reader.configuration.readonly)
do {
// Open a long-lived transaction, and enter snapshot isolation
self.walSnapshot = try reader.sync(allowingLongLivedTransaction: true) { db in
try db.beginTransaction(.deferred)
// This also acquires snapshot isolation because checking
// database schema performs a read access.
try db.clearSchemaCacheIfNeeded()
return try WALSnapshot(db)
}
self.reader = reader
self.release = release
} catch {
// self is not initialized, so deinit will not run.
Self.commitAndRelease(reader: reader, release: release)
throw error
}
}
deinit {
Self.commitAndRelease(reader: reader, release: release)
}
/// Executes database operations in the snapshot transaction, and
/// returns their result after they have finished executing.
func read<T>(_ value: (Database) throws -> T) rethrows -> T {
// We should check the validity of the snapshot, as DatabaseSnapshotPool does.
try reader.sync(value)
}
/// Schedules database operations for execution, and
/// returns immediately.
func asyncRead(_ value: @escaping (Database) -> Void) {
// We should check the validity of the snapshot, as DatabaseSnapshotPool does.
reader.async(value)
}
private static func commitAndRelease(
reader: SerializedDatabase,
release: (_ isInsideTransaction: Bool) -> Void)
{
// WALSnapshotTransaction may be deinitialized in the dispatch
// queue of its reader: allow reentrancy.
let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in
try? db.commit()
return db.isInsideTransaction
}
release(isInsideTransaction)
}
}
#endif