add swiftUI code
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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 once—when
|
||||
/// `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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,352 @@
|
||||
# Concurrency
|
||||
|
||||
GRDB helps your app deal with SQLite concurrency.
|
||||
|
||||
## Overview
|
||||
|
||||
If your app moves slow database jobs off the main thread, so that the user interface remains responsive, then this guide is for you. In the case of apps that share a database with other processes, such as an iOS app and its extensions, don't miss the dedicated <doc:DatabaseSharing> guide after this one.
|
||||
|
||||
**In all cases, and first and foremost, follow the <doc:Concurrency#Concurrency-Rules> right from the start.**
|
||||
|
||||
The other chapters cover, with more details, the fundamentals of SQLite concurrency, and how GRDB makes it manageable from your Swift code.
|
||||
|
||||
## Concurrency Rules
|
||||
|
||||
**The two concurrency rules are strongly recommended practices.** They are all about SQLite, a robust and reliable database that takes great care of your data: don't miss an opportunity to put it on your side!
|
||||
|
||||
#### Rule 1: Connect to any database file only once
|
||||
|
||||
Open one single ``DatabaseQueue`` or ``DatabasePool`` per database file, for the whole duration of your use of the database. Not for the duration of _each_ database access, but really for the duration of _all_ database accesses to this file.
|
||||
|
||||
- *Why does this rule exist?* - Since SQLite does not support parallel writes, each `DatabaseQueue` and `DatabasePool` makes sure application threads perform writes one by one, without overlap.
|
||||
|
||||
- *Practical advice* - An app that uses a single database will connect only once. A document-based app will connect each time a document is opened, and disconnect when the document is closed. See the [demo apps] in order to see how to setup a UIKit or SwiftUI application for a single database.
|
||||
|
||||
- *What if you do not follow this rule?*
|
||||
|
||||
- You will not be able to use the <doc:DatabaseObservation> features.
|
||||
- You will see SQLite errors ([`SQLITE_BUSY`]).
|
||||
|
||||
#### Rule 2: Mind your transactions
|
||||
|
||||
Database operations that are grouped in a transaction are guaranteed to be either fully saved on disk, or not at all. Read-only transactions guarantee a stable and immutable view of the database, and do not see changes performed by eventual concurrent writes.
|
||||
|
||||
In other words, transactions are the one and single tool that helps you enforce and rely on the invariants of your database (such as "all authors must have at least one book").
|
||||
|
||||
**You are responsible**, in your Swift code, for delimiting transactions. You do so by grouping database accesses inside a pair of `{ db in ... }` brackets:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Inside a transaction
|
||||
}
|
||||
|
||||
try dbQueue.read { db
|
||||
// Inside a transaction
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can open an explicit transaction or savepoint: see <doc:Transactions>.
|
||||
|
||||
- *Why does this rule exist?* - Because GRDB and SQLite can not guess where to insert the transaction boundaries that protect the invariants of your database. This is your task. Transactions also avoid concurrency problems, as described in the <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> section below.
|
||||
|
||||
- *Practical advice* - Take the time to identify the invariants of your database. Some of them can be enforced in the database schema itself, such as "all books must have a non-empty title", or "all books must have an author" (see <doc:DatabaseSchema>). Some invariants can only be enforced by transactions, such as "all account credits must have a matching debit", or "all authors must have at least one book".
|
||||
|
||||
- *What if you do not follow this rule?* - You will see broken database invariants, at runtime, or when your apps wakes up after a crash. These bugs corrupt user data, and are very difficult to fix.
|
||||
|
||||
|
||||
## Synchronous and Asynchronous Database Accesses
|
||||
|
||||
**You can access the database from any thread, in a synchronous or asynchronous way.**
|
||||
|
||||
➡️ **A sync access blocks the current thread** until the database operations are completed:
|
||||
|
||||
```swift
|
||||
let playerCount = try dbQueue.read { db in
|
||||
try Player.fetchCount(db)
|
||||
}
|
||||
|
||||
let newPlayerCount = try dbQueue.write { db -> Int in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
See ``DatabaseReader/read(_:)-3806d`` and ``DatabaseWriter/write(_:)-76inz``.
|
||||
|
||||
It is a programmer error to perform a sync access from any other database access (this restriction can be lifted: see <doc:Concurrency#Safe-and-Unsafe-Database-Accesses>):
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Fatal Error: Database methods are not reentrant.
|
||||
try dbQueue.write { db in ... }
|
||||
}
|
||||
```
|
||||
|
||||
🔀 **An async access does not block the current thread.** Instead, it notifies you when the database operations are completed. There are four ways to access the database asynchronously:
|
||||
|
||||
- **Swift concurrency** (async/await)
|
||||
|
||||
[**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
|
||||
```swift
|
||||
let playerCount = try await dbQueue.read { db in
|
||||
try Player.fetchCount(db)
|
||||
}
|
||||
|
||||
let newPlayerCount = try await dbQueue.write { db -> Int in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
See ``DatabaseReader/read(_:)-4w6gy`` and ``DatabaseWriter/write(_:)-88g7e``.
|
||||
|
||||
Note the identical method names: `read`, `write`. The async version is only available in async Swift functions.
|
||||
|
||||
- **Combine publishers**
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
let playerCountPublisher = dbQueue.readPublisher { db in
|
||||
try Player.fetchCount(db)
|
||||
}
|
||||
|
||||
let newPlayerCountPublisher = dbQueue.writePublisher { db -> Int in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
See ``DatabaseReader/readPublisher(receiveOn:value:)``, and ``DatabaseWriter/writePublisher(receiveOn:updates:)``.
|
||||
|
||||
Those publishers do not access the database until they are subscribed. They complete on the main dispatch queue by default.
|
||||
|
||||
- **RxSwift observables**
|
||||
|
||||
See the companion library [RxGRDB].
|
||||
|
||||
- **Completion blocks**
|
||||
|
||||
See ``DatabaseReader/asyncRead(_:)`` and ``DatabaseWriter/asyncWrite(_:completion:)``.
|
||||
|
||||
During one async access, all individual database operations grouped inside (fetch, insert, etc.) are synchronous:
|
||||
|
||||
```swift
|
||||
// One asynchronous access...
|
||||
try await dbQueue.write { db in
|
||||
// ... always performs synchronous database operations:
|
||||
try Player(...).insert(db)
|
||||
try Player(...).insert(db)
|
||||
let players = try Player.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
This is true for all async techniques.
|
||||
|
||||
This prevents the database operations from various concurrent accesses from being interleaved. For example, one access must not be able to issue a `COMMIT` statement in the middle of an unfinished concurrent write!
|
||||
|
||||
## Safe and Unsafe Database Accesses
|
||||
|
||||
**You will generally use the safe database access methods `read` and `write`.** In this context, "safe" means that a database access is concurrency-friendly, because GRDB provides the following guarantees:
|
||||
|
||||
#### Serialized Writes
|
||||
|
||||
**All writes performed by one ``DatabaseQueue`` or ``DatabasePool`` instance are serialized.**
|
||||
|
||||
This guarantee prevents [`SQLITE_BUSY`] errors during concurrent writes.
|
||||
|
||||
#### Write Transactions
|
||||
|
||||
**All writes are wrapped in a transaction.**
|
||||
|
||||
Concurrent reads can not see partial database updates (even reads performed by other processes).
|
||||
|
||||
#### Isolated Reads
|
||||
|
||||
**All reads are wrapped in a transaction.**
|
||||
|
||||
An isolated read sees a stable and immutable state of the database, and does not see changes performed by eventual concurrent writes (even writes performed by other processes). See [Isolation In SQLite](https://www.sqlite.org/isolation.html) for more information.
|
||||
|
||||
#### Forbidden Writes
|
||||
|
||||
**Inside a read access, all attempts to write raise an error.**
|
||||
|
||||
This enforces the immutability of the database during a read.
|
||||
|
||||
#### Non-Reentrancy
|
||||
|
||||
**Database accesses methods are not reentrant.**
|
||||
|
||||
This reduces the opportunities for deadlocks, and fosters the clear transaction boundaries of <doc:Concurrency#Rule-2:-Mind-your-transactions>.
|
||||
|
||||
### Unsafe Database Accesses
|
||||
|
||||
Some applications need to relax this safety net, in order to achieve specific SQLite operations. In this case, replace `read` and `write` with one of the methods below:
|
||||
|
||||
- **Write outside of any transaction** (Lifted guarantee: <doc:Concurrency#Write-Transactions>)
|
||||
|
||||
See all ``DatabaseWriter`` methods with `WithoutTransaction` in their names.
|
||||
|
||||
- **Reentrant write, outside of any transaction** (Lifted guarantees: <doc:Concurrency#Write-Transactions>, <doc:Concurrency#Non-Reentrancy>)
|
||||
|
||||
See ``DatabaseWriter/unsafeReentrantWrite(_:)``.
|
||||
|
||||
- **Read outside of any transaction** (Lifted guarantees: <doc:Concurrency#Isolated-Reads>, <doc:Concurrency#Forbidden-Writes>)
|
||||
|
||||
See all ``DatabaseReader`` methods with `unsafe` in their names.
|
||||
|
||||
- **Reentrant read, outside of any transaction** (Lifted guarantees: <doc:Concurrency#Isolated-Reads>, <doc:Concurrency#Forbidden-Writes>, <doc:Concurrency#Non-Reentrancy>)
|
||||
|
||||
See ``DatabaseReader/unsafeReentrantRead(_:)``.
|
||||
|
||||
> Important: By using one of the methods above, you become responsible of the thread-safety of your application. Please understand the consequences of lifting each concurrency guarantee.
|
||||
|
||||
Some concurrency guarantees can be restored at your convenience:
|
||||
|
||||
- The <doc:Concurrency#Write-Transactions> and <doc:Concurrency#Isolated-Reads> guarantees can be restored at any point, with an explicit transaction or savepoint. For example:
|
||||
|
||||
```swift
|
||||
try dbQueue.writeWithoutTransaction { db in
|
||||
try db.inTransaction { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- The <doc:Concurrency#Forbidden-Writes> guarantee can only be lifted with ``DatabaseQueue``. It can be restored with [`PRAGMA query_only`](https://www.sqlite.org/pragma.html#pragma_query_only).
|
||||
|
||||
## Differences between Database Queues and Pools
|
||||
|
||||
Despite the common guarantees and rules shared by database queues and pools, those two database accessors don't have the same behavior.
|
||||
|
||||
``DatabaseQueue`` opens a single database connection, and serializes all database accesses, reads, and writes. There is never more than one thread that uses the database. In the image below, we see how three threads can see the database as time passes:
|
||||
|
||||

|
||||
|
||||
``DatabasePool`` manages a pool of several database connections, and allows concurrent reads and writes thanks to the [WAL mode](https://www.sqlite.org/wal.html). A database pool serializes all writes (the <doc:Concurrency#Serialized-Writes> guarantee). Reads are isolated so that they don't see changes performed by other threads (the <doc:Concurrency#Isolated-Reads> guarantee). This gives a very different picture:
|
||||
|
||||

|
||||
|
||||
See how, with database pools, two reads can see different database states at the same time. This may look scary! Please see the next chapter below for a relief.
|
||||
|
||||
## Concurrent Thinking
|
||||
|
||||
Despite the <doc:Concurrency#Differences-between-Database-Queues-and-Pools>, you can write robust code that works equally well with both `DatabaseQueue` and `DatabasePool`.
|
||||
|
||||
This allows your app to switch between queues and pools, at your convenience:
|
||||
|
||||
- The [demo applications] share the same database code for the on-disk pool that feeds the app, and the in-memory queue that feeds tests and SwiftUI previews. This makes sure tests and previews run fast, without any temporary file, with the same behavior as the app.
|
||||
|
||||
- Applications that perform slow write transactions (when saving a lot of data from a remote server, for example) may want to replace their queue with a pool so that the reads that feed their user interface can run in parallel.
|
||||
|
||||
All you need is a little "concurrent thinking", based on those two basic facts:
|
||||
|
||||
- You are sure, when you perform a write access, that you deal with the latest database state on disk. This is enforced by SQLite, which simply can't perform parallel writes, and by the <doc:Concurrency#Serialized-Writes> guarantee. Writes performed by other processes can trigger an [`SQLITE_BUSY`] ``DatabaseError`` that you can handle.
|
||||
|
||||
- Whenever you extract some data from a database access, immediately consider it as _stale_. It is stale, whether you use a `DatabaseQueue` or `DatabasePool`. It is stale because nothing prevents other application threads or processes from overwriting the value you have just fetched:
|
||||
|
||||
```swift
|
||||
// or dbQueue.write, for that matter
|
||||
let cookieCount = dbPool.read { db in
|
||||
try Cookie.fetchCount(db)
|
||||
}
|
||||
|
||||
// At this point, the number of cookies on disk
|
||||
// may have already changed.
|
||||
print("We have \(cookieCount) cookies left")
|
||||
```
|
||||
|
||||
Does this mean you can't rely on anything? Of course not:
|
||||
|
||||
- If you intend to display some database value on screen, use ``ValueObservation``: it always eventually notifies the latest state of the database. Your application won't display stale values for a long time: after the database has been changed on disk, the fresh value if fetched, and soon notified on the main thread where the screen can be updated.
|
||||
|
||||
- As said above, the moment of truth is the next write access!
|
||||
|
||||
## Advanced DatabasePool
|
||||
|
||||
``DatabasePool`` is very concurrent, since all reads can run in parallel, and can even run during write operations. But writes are still serialized: at any given point in time, there is no more than a single thread that is writing into the database.
|
||||
|
||||
When your application modifies the database, and then reads some value that depends on those modifications, you may want to avoid blocking concurrent writes longer than necessary - especially when the read is slow:
|
||||
|
||||
```swift
|
||||
let newPlayerCount = try dbPool.write { db in
|
||||
// Increment the number of players
|
||||
try Player(...).insert(db)
|
||||
|
||||
// Read the number of players. Concurrent writes are blocked :-(
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
➡️ The synchronous solution is the ``DatabaseWriter/concurrentRead(_:)`` method. It must be called from within a write access, outside of any transaction. It returns a ``DatabaseFuture`` which you consume any time later, with the ``DatabaseFuture/wait()`` method:
|
||||
|
||||
```swift
|
||||
let future: DatabaseFuture<Int> = try dbPool.writeWithoutTransaction { db in
|
||||
// Increment the number of players
|
||||
try db.inTransaction {
|
||||
try Player(...).insert(db)
|
||||
return .commit
|
||||
}
|
||||
|
||||
// <- Not in a transaction here
|
||||
return dbPool.concurrentRead { db
|
||||
try Player.fetchCount(db)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
// Handle the new player count - guaranteed greater than zero
|
||||
let newPlayerCount = try future.wait()
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
🔀 The asynchronous version of `concurrentRead` is ``DatabasePool/asyncConcurrentRead(_:)``:
|
||||
|
||||
```swift
|
||||
try dbPool.writeWithoutTransaction { db in
|
||||
// Increment the number of players
|
||||
try db.inTransaction {
|
||||
try Player(...).insert(db)
|
||||
return .commit
|
||||
}
|
||||
|
||||
// <- Not in a transaction here
|
||||
dbPool.asyncConcurrentRead { dbResult in
|
||||
do {
|
||||
// Handle the new player count - guaranteed greater than zero
|
||||
let db = try dbResult.get()
|
||||
let newPlayerCount = try Player.fetchCount(db)
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Both ``DatabaseWriter/concurrentRead(_:)`` and ``DatabasePool/asyncConcurrentRead(_:)`` block until they can guarantee their closure argument an isolated access to the database, in the exact state left by the last transaction. It then asynchronously executes this closure.
|
||||
|
||||
In the illustration below, the striped band shows the delay needed for the reading thread to acquire isolation. Until then, no other thread can write:
|
||||
|
||||
|
||||

|
||||
|
||||
Types that conform to ``TransactionObserver`` can also use those methods in their ``TransactionObserver/databaseDidCommit(_:)`` method, in order to process database changes without blocking other threads that want to write into the database.
|
||||
|
||||
## Topics
|
||||
|
||||
### Database Connections with Concurrency Guarantees
|
||||
|
||||
- ``DatabaseWriter``
|
||||
- ``DatabaseReader``
|
||||
- ``DatabaseSnapshotReader``
|
||||
|
||||
### Advanced Concurrency
|
||||
|
||||
- <doc:DatabaseSharing>
|
||||
|
||||
|
||||
[demo apps]: https://github.com/groue/GRDB.swift/tree/master/Documentation/DemoApps
|
||||
[`SQLITE_BUSY`]: https://www.sqlite.org/rescode.html#busy
|
||||
[RxGRDB]: https://github.com/RxSwiftCommunity/RxGRDB
|
||||
[demo applications]: https://github.com/groue/GRDB.swift/tree/master/Documentation/DemoApps
|
||||
@@ -0,0 +1,111 @@
|
||||
# Database Connections
|
||||
|
||||
Open database connections to SQLite databases.
|
||||
|
||||
## Overview
|
||||
|
||||
GRDB provides two classes for accessing SQLite databases: ``DatabaseQueue`` and ``DatabasePool``:
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
|
||||
// Pick one:
|
||||
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
|
||||
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
||||
```
|
||||
|
||||
The differences are:
|
||||
|
||||
- `DatabasePool` allows concurrent database accesses (this can improve the performance of multithreaded applications).
|
||||
- `DatabasePool` opens your SQLite database in the [WAL mode](https://www.sqlite.org/wal.html).
|
||||
- `DatabaseQueue` supports <doc:DatabaseQueue#In-Memory-Databases>.
|
||||
|
||||
**If you are not sure, choose `DatabaseQueue`.** You will always be able to switch to `DatabasePool` later.
|
||||
|
||||
## Opening a Connection
|
||||
|
||||
You need a path to a database file in order to open a database connection.
|
||||
|
||||
**When the SQLite file is ready-made, and you do not intend to modify its content**, then add the database file as a [resource of your Xcode project or Swift package](https://developer.apple.com/documentation/xcode), and open a read-only database connection:
|
||||
|
||||
```swift
|
||||
// HOW TO open a read-only connection to a database resource
|
||||
|
||||
// Get the path to the database resource.
|
||||
// Replace `Bundle.main` with `Bundle.module` when you write a Swift Package.
|
||||
if let dbPath = Bundle.main.path(forResource: "db", ofType: "sqlite")
|
||||
|
||||
if let dbPath {
|
||||
// If the resource exists, open a read-only connection.
|
||||
// Writes are disallowed because resources can not be modified.
|
||||
var config = Configuration()
|
||||
config.readonly = true
|
||||
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)
|
||||
} else {
|
||||
// The database resource can not be found.
|
||||
// Fix your setup, or report the problem to the user.
|
||||
}
|
||||
```
|
||||
|
||||
**If the application creates or writes in the database**, then first choose a proper location for the database file. Document-based applications will let the user pick a location. Apps that use the database as a global storage will prefer the Application Support directory.
|
||||
|
||||
> Tip: Regardless of the database location, it is recommended that you wrap the database file inside a dedicated directory. This directory will bundle the main database file and its related [SQLite temporary files](https://www.sqlite.org/tempfiles.html) together.
|
||||
>
|
||||
> The dedicated directory helps moving or deleting the whole database when needed: just move or delete the directory.
|
||||
>
|
||||
> On iOS, the directory can be encrypted with [data protection](https://developer.apple.com/documentation/uikit/protecting_the_user_s_privacy/encrypting_your_app_s_files), in order to help securing all database files in one shot. When a database is protected, an application that runs in the background on a locked device won't be able to read or write from it. Instead, it will catch ``DatabaseError`` with code [`SQLITE_IOERR`](https://www.sqlite.org/rescode.html#ioerr) (10) "disk I/O error", or [`SQLITE_AUTH`](https://www.sqlite.org/rescode.html#auth) (23) "not authorized".
|
||||
|
||||
The sample code below creates or opens a database file inside its dedicated directory. On the first run, a new empty database file is created. On subsequent runs, the directory and database file already exist, so it just opens a connection:
|
||||
|
||||
```swift
|
||||
// HOW TO create an empty database, or open an existing database file
|
||||
|
||||
// Create the "Application Support/MyDatabase" directory if needed
|
||||
let fileManager = FileManager.default
|
||||
let appSupportURL = try fileManager.url(
|
||||
for: .applicationSupportDirectory, in: .userDomainMask,
|
||||
appropriateFor: nil, create: true)
|
||||
let directoryURL = appSupportURL.appendingPathComponent("MyDatabase", isDirectory: true)
|
||||
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||
|
||||
// Open or create the database
|
||||
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
|
||||
let dbQueue = try DatabaseQueue(path: databaseURL.path)
|
||||
```
|
||||
|
||||
## Closing Connections
|
||||
|
||||
Database connections are automatically closed when ``DatabaseQueue`` or ``DatabasePool`` instances are deinitialized.
|
||||
|
||||
If the correct execution of your program depends on precise database closing, perform an explicit call to ``DatabaseReader/close()``. This method may fail and create zombie connections, so please check its detailed documentation.
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once connected to the database, your next steps are probably:
|
||||
|
||||
- Define the structure of newly created databases: see <doc:Migrations>.
|
||||
- If you intend to write SQL, see <doc:SQLSupport>. Otherwise, see <doc:QueryInterface>.
|
||||
|
||||
Even if you plan to keep your project mundane and simple, take the time to read the <doc:Concurrency> guide eventually.
|
||||
|
||||
## Topics
|
||||
|
||||
### Configuring database connections
|
||||
|
||||
- ``Configuration``
|
||||
|
||||
### Connections for read and write accesses
|
||||
|
||||
- ``DatabaseQueue``
|
||||
- ``DatabasePool``
|
||||
|
||||
### Read-only connections on an unchanging database content
|
||||
|
||||
- ``DatabaseSnapshot``
|
||||
- ``DatabaseSnapshotPool``
|
||||
|
||||
### Using database connections
|
||||
|
||||
- ``Database``
|
||||
- ``DatabaseError``
|
||||
@@ -0,0 +1,42 @@
|
||||
# Database Observation
|
||||
|
||||
Observe database changes and transactions.
|
||||
|
||||
## Overview
|
||||
|
||||
**SQLite notifies its host application of changes performed to the database, as well of transaction commits and rollbacks.**
|
||||
|
||||
GRDB puts this SQLite feature to some good use, and lets you observe the database in various ways:
|
||||
|
||||
- ``ValueObservation``: Get notified when database values change.
|
||||
- ``DatabaseRegionObservation``: Get notified when a transaction impacts a database region.
|
||||
- ``Database/afterNextTransaction(onCommit:onRollback:)``: Handle transactions commits or rollbacks, one by one.
|
||||
- ``TransactionObserver``: The low-level protocol that supports all database observation features.
|
||||
|
||||
## Topics
|
||||
|
||||
### Observing Database Values
|
||||
|
||||
- ``ValueObservation``
|
||||
- ``SharedValueObservation``
|
||||
- ``AsyncValueObservation``
|
||||
- ``Database/registerAccess(to:)``
|
||||
|
||||
### Observing Database Transactions
|
||||
|
||||
- ``DatabaseRegionObservation``
|
||||
- ``Database/afterNextTransaction(onCommit:onRollback:)``
|
||||
|
||||
### Low-Level Transaction Observers
|
||||
|
||||
- ``TransactionObserver``
|
||||
- ``Database/add(transactionObserver:extent:)``
|
||||
- ``Database/remove(transactionObserver:)``
|
||||
- ``DatabaseWriter/add(transactionObserver:extent:)``
|
||||
- ``DatabaseWriter/remove(transactionObserver:)``
|
||||
- ``Database/TransactionObservationExtent``
|
||||
|
||||
### Database Regions
|
||||
|
||||
- ``DatabaseRegion``
|
||||
- ``DatabaseRegionConvertible``
|
||||
@@ -0,0 +1,423 @@
|
||||
# The Database Schema
|
||||
|
||||
Define or query the database schema.
|
||||
|
||||
## Overview
|
||||
|
||||
**GRDB supports all database schemas, and has no requirement.** Any existing SQLite database can be opened, and you are free to structure your new databases as you wish.
|
||||
|
||||
You perform modifications to the database schema with methods such as ``Database/create(table:options:body:)``, listed at the end of this page. For example:
|
||||
|
||||
```swift
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
}
|
||||
```
|
||||
|
||||
When you plan to evolve the schema as new versions of your application ship, wrap all schema changes in <doc:Migrations>.
|
||||
|
||||
Prefer Swift methods over raw SQL queries. They allow the compiler to check if a schema change is available on the target operating system. Only use a raw SQL query when no Swift method exist (when creating triggers, for example).
|
||||
|
||||
When a schema change is not directly supported by SQLite, or not available on the target operating system, database tables have to be recreated. See <doc:Migrations> for the detailed procedure.
|
||||
|
||||
## Database Schema Recommendations
|
||||
|
||||
Even though all schema are supported, some features of the library and of the Swift language are easier to use when the schema follows a few conventions described below.
|
||||
|
||||
When those conventions are not applied, or not applicable, you will have to perform extra configurations.
|
||||
|
||||
For recommendations specific to JSON columns, see <doc:JSON>.
|
||||
|
||||
### Table names should be English, singular, and camelCased
|
||||
|
||||
Make them look like singular Swift identifiers: `player`, `team`, `postalAddress`:
|
||||
|
||||
```swift
|
||||
// RECOMMENDED
|
||||
try db.create(table: "player") { t in
|
||||
// table columns and constraints
|
||||
}
|
||||
|
||||
// REQUIRES EXTRA CONFIGURATION
|
||||
try db.create(table: "players") { t in
|
||||
// table columns and constraints
|
||||
}
|
||||
```
|
||||
|
||||
☝️ **If table names follow a different naming convention**, record types (see <doc:QueryInterface>) will need explicit table names:
|
||||
|
||||
```swift
|
||||
extension Player: TableRecord {
|
||||
// Required because table name is not 'player'
|
||||
static let databaseTableName = "players"
|
||||
}
|
||||
|
||||
extension PostalAddress: TableRecord {
|
||||
// Required because table name is not 'postalAddress'
|
||||
static let databaseTableName = "postal_address"
|
||||
}
|
||||
|
||||
extension Award: TableRecord {
|
||||
// Required because table name is not 'award'
|
||||
static let databaseTableName = "Auszeichnung"
|
||||
}
|
||||
```
|
||||
|
||||
[Associations](https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md) will need explicit keys as well:
|
||||
|
||||
```swift
|
||||
extension Player: TableRecord {
|
||||
// Explicit association key because the table name is not 'postalAddress'
|
||||
static let postalAddress = belongsTo(PostalAddress.self, key: "postalAddress")
|
||||
|
||||
// Explicit association key because the table name is not 'award'
|
||||
static let awards = hasMany(Award.self, key: "awards")
|
||||
}
|
||||
```
|
||||
|
||||
As in the above example, make sure to-one associations use singular keys, and to-many associations use plural keys.
|
||||
|
||||
### Column names should be camelCased
|
||||
|
||||
Again, make them look like Swift identifiers: `fullName`, `score`, `creationDate`:
|
||||
|
||||
```swift
|
||||
// RECOMMENDED
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("fullName", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
t.column("creationDate", .datetime).notNull()
|
||||
}
|
||||
|
||||
// REQUIRES EXTRA CONFIGURATION
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("full_name", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
t.column("creation_date", .datetime).notNull()
|
||||
}
|
||||
```
|
||||
|
||||
☝️ **If the column names follow a different naming convention**, `Codable` record types will need an explicit `CodingKeys` enum:
|
||||
|
||||
```swift
|
||||
struct Player: Decodable, FetchableRecord {
|
||||
var id: Int64
|
||||
var fullName: String
|
||||
var score: Int
|
||||
var creationDate: Date
|
||||
|
||||
// Required CodingKeys customization because
|
||||
// columns are not named like Swift properties
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, fullName = "full_name", score, creationDate = "creation_date"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Tables should have explicit primary keys
|
||||
|
||||
A primary key uniquely identifies a row in a table. It is defined on one or several columns:
|
||||
|
||||
```swift
|
||||
// RECOMMENDED
|
||||
try db.create(table: "player") { t in
|
||||
// Auto-incremented primary key
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
|
||||
try db.create(table: "team") { t in
|
||||
// Single-column primary key
|
||||
t.primaryKey("id", .text)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
|
||||
try db.create(table: "membership") { t in
|
||||
// Composite primary key
|
||||
t.primaryKey {
|
||||
t.belongsTo("player")
|
||||
t.belongsTo("team")
|
||||
}
|
||||
t.column("role", .text).notNull()
|
||||
}
|
||||
```
|
||||
|
||||
Primary keys support record fetching methods such as ``FetchableRecord/fetchOne(_:id:)``, and persistence methods such as ``MutablePersistableRecord/update(_:onConflict:)`` or ``MutablePersistableRecord/delete(_:)``.
|
||||
|
||||
See <doc:SingleRowTables> when you need to define a table that contains a single row.
|
||||
|
||||
☝️ **If the database table does not define any explicit primary key**, identifying specific rows in this table needs explicit support for the [hidden `rowid` column](https://www.sqlite.org/rowidtable.html) in the matching record types:
|
||||
|
||||
```swift
|
||||
// A table without any explicit primary key
|
||||
try db.create(table: "player") { t in
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
}
|
||||
|
||||
// The record type for the 'player' table'
|
||||
struct Player: Codable {
|
||||
// Uniquely identifies a player.
|
||||
var rowid: Int64?
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
|
||||
extension Player: FetchableRecord, MutablePersistableRecord {
|
||||
// Required because the primary key
|
||||
// is the hidden rowid column.
|
||||
static let databaseSelection: [any SQLSelectable] = [
|
||||
AllColumns(),
|
||||
Column.rowID]
|
||||
|
||||
// Update id upon successful insertion
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
rowid = inserted.rowID
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.read { db in
|
||||
// SELECT *, rowid FROM player WHERE rowid = 1
|
||||
if let player = try Player.fetchOne(db, id: 1) {
|
||||
// DELETE FROM player WHERE rowid = 1
|
||||
let deleted = try player.delete(db)
|
||||
print(deleted) // true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Single-column primary keys should be named 'id'
|
||||
|
||||
This helps record types play well with the standard `Identifiable` protocol.
|
||||
|
||||
```swift
|
||||
// RECOMMENDED
|
||||
try db.create(table: "player") { t in
|
||||
t.primaryKey("id", .text)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
|
||||
// REQUIRES EXTRA CONFIGURATION
|
||||
try db.create(table: "player") { t in
|
||||
t.primaryKey("uuid", .text)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
```
|
||||
☝️ **If the primary key follows a different naming convention**, `Identifiable` record types will need a custom `CodingKeys` enum, or an extra property:
|
||||
|
||||
```swift
|
||||
// Custom coding keys
|
||||
struct Player: Codable, Identifiable {
|
||||
var id: String
|
||||
var name: String
|
||||
|
||||
// Required CodingKeys customization because
|
||||
// columns are not named like Swift properties
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "uuid", name
|
||||
}
|
||||
}
|
||||
|
||||
// Extra property
|
||||
struct Player: Identifiable {
|
||||
var uuid: String
|
||||
var name: String
|
||||
|
||||
// Required because the primary key column is not 'id'
|
||||
var id: String { uuid }
|
||||
}
|
||||
```
|
||||
|
||||
### Unique keys should be supported by unique indexes
|
||||
|
||||
Unique indexes makes sure SQLite prevents the insertion of conflicting rows:
|
||||
|
||||
```swift
|
||||
// RECOMMENDED
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("team").notNull()
|
||||
t.column("position", .integer).notNull()
|
||||
// Players must have distinct names
|
||||
t.column("name", .text).unique()
|
||||
}
|
||||
|
||||
// One single player at any given position in a team
|
||||
try db.create(
|
||||
indexOn: "player",
|
||||
columns: ["teamId", "position"],
|
||||
options: .unique)
|
||||
```
|
||||
|
||||
> Tip: SQLite does not support deferred unique indexes, and this creates undesired churn when you need to temporarily break them. This may happen, for example, when you want to reorder player positions in our above example.
|
||||
>
|
||||
> There exist several workarounds; one of them involves dropping and recreating the unique index after the temporary violations have been fixed. If you plan to use this technique, take care that only actual indexes can be dropped. Unique constraints created inside the table body can not:
|
||||
>
|
||||
> ```swift
|
||||
> // Unique constraint on player(name) can not be dropped.
|
||||
> try db.create(table: "player") { t in
|
||||
> t.column("name", .text).unique()
|
||||
> }
|
||||
>
|
||||
> // Unique index on team(name) can be dropped.
|
||||
> try db.create(table: "team") { t in
|
||||
> t.column("name", .text)
|
||||
> }
|
||||
> try db.create(indexOn: "team", columns: ["name"], options: .unique)
|
||||
> ```
|
||||
>
|
||||
> If you want to turn an undroppable constraint into a droppable index, you'll need to recreate the database table. See <doc:Migrations> for the detailed procedure.
|
||||
|
||||
☝️ **If a table misses unique indexes**, some record methods such as ``FetchableRecord/fetchOne(_:key:)-92b9m`` and ``TableRecord/deleteOne(_:key:)-5pdh5`` will raise a fatal error:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Fatal error: table player has no unique index on columns ...
|
||||
let player = try Player.fetchOne(db, key: ["teamId": 42, "position": 1])
|
||||
try Player.deleteOne(db, key: ["name": "Arthur"])
|
||||
|
||||
// Use instead:
|
||||
let player = try Player
|
||||
.filter(Column("teamId") == 42 && Column("position") == 1)
|
||||
.fetchOne(db)
|
||||
|
||||
try Player
|
||||
.filter(Column("name") == "Arthur")
|
||||
.deleteAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
### Relations between tables should be supported by foreign keys
|
||||
|
||||
[Foreign Keys](https://www.sqlite.org/foreignkeys.html) have SQLite enforce valid relationships between tables:
|
||||
|
||||
```swift
|
||||
try db.create(table: "team") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("color", .text).notNull()
|
||||
}
|
||||
|
||||
// RECOMMENDED
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
// A player must refer to an existing team
|
||||
t.belongsTo("team").notNull()
|
||||
}
|
||||
|
||||
// REQUIRES EXTRA CONFIGURATION
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
// No foreign key
|
||||
t.column("teamId", .integer).notNull()
|
||||
}
|
||||
```
|
||||
|
||||
See ``TableDefinition/belongsTo(_:inTable:onDelete:onUpdate:deferred:indexed:)`` for more information about the creation of foreign keys.
|
||||
|
||||
GRDB [Associations](https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md) are automatically configured from foreign keys declared in the database schema:
|
||||
|
||||
```swift
|
||||
extension Player: TableRecord {
|
||||
static let team = belongsTo(Team.self)
|
||||
}
|
||||
|
||||
extension Team: TableRecord {
|
||||
static let players = hasMany(Player.self)
|
||||
}
|
||||
```
|
||||
|
||||
See [Associations and the Database Schema](https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md#associations-and-the-database-schema) for more precise recommendations.
|
||||
|
||||
☝️ **If a foreign key is not declared in the schema**, you will need to explicitly configure related associations:
|
||||
|
||||
```swift
|
||||
extension Player: TableRecord {
|
||||
// Required configuration because the database does
|
||||
// not declare any foreign key from players to their team.
|
||||
static let teamForeignKey = ForeignKey(["teamId"])
|
||||
static let team = belongsTo(Team.self,
|
||||
using: teamForeignKey)
|
||||
}
|
||||
|
||||
extension Team: TableRecord {
|
||||
// Required configuration because the database does
|
||||
// not declare any foreign key from players to their team.
|
||||
static let players = hasMany(Player.self,
|
||||
using: Player.teamForeignKey)
|
||||
}
|
||||
```
|
||||
|
||||
## Topics
|
||||
|
||||
### Database Tables
|
||||
|
||||
- ``Database/alter(table:body:)``
|
||||
- ``Database/create(table:options:body:)``
|
||||
- ``Database/create(virtualTable:ifNotExists:using:)``
|
||||
- ``Database/create(virtualTable:ifNotExists:using:_:)``
|
||||
- ``Database/drop(table:)``
|
||||
- ``Database/dropFTS4SynchronizationTriggers(forTable:)``
|
||||
- ``Database/dropFTS5SynchronizationTriggers(forTable:)``
|
||||
- ``Database/rename(table:to:)``
|
||||
- ``Database/ColumnType``
|
||||
- ``Database/ConflictResolution``
|
||||
- ``Database/ForeignKeyAction``
|
||||
- ``TableAlteration``
|
||||
- ``TableDefinition``
|
||||
- ``TableOptions``
|
||||
- ``VirtualTableModule``
|
||||
|
||||
### Database Views
|
||||
|
||||
- ``Database/create(view:options:columns:as:)``
|
||||
- ``Database/create(view:options:columns:asLiteral:)``
|
||||
- ``Database/drop(view:)``
|
||||
- ``ViewOptions``
|
||||
|
||||
### Database Indexes
|
||||
|
||||
- ``Database/create(indexOn:columns:options:condition:)``
|
||||
- ``Database/create(index:on:columns:options:condition:)``
|
||||
- ``Database/create(index:on:expressions:options:condition:)``
|
||||
- ``Database/drop(indexOn:columns:)``
|
||||
- ``Database/drop(index:)``
|
||||
- ``IndexOptions``
|
||||
|
||||
### Querying the Database Schema
|
||||
|
||||
- ``Database/columns(in:in:)``
|
||||
- ``Database/foreignKeys(on:in:)``
|
||||
- ``Database/indexes(on:in:)``
|
||||
- ``Database/isGRDBInternalTable(_:)``
|
||||
- ``Database/isSQLiteInternalTable(_:)``
|
||||
- ``Database/primaryKey(_:in:)``
|
||||
- ``Database/schemaVersion()``
|
||||
- ``Database/table(_:hasUniqueKey:)``
|
||||
- ``Database/tableExists(_:in:)``
|
||||
- ``Database/triggerExists(_:in:)``
|
||||
- ``Database/viewExists(_:in:)``
|
||||
- ``ColumnInfo``
|
||||
- ``ForeignKeyInfo``
|
||||
- ``IndexInfo``
|
||||
- ``PrimaryKeyInfo``
|
||||
|
||||
### Integrity Checks
|
||||
|
||||
- ``Database/checkForeignKeys()``
|
||||
- ``Database/checkForeignKeys(in:in:)``
|
||||
- ``Database/foreignKeyViolations()``
|
||||
- ``Database/foreignKeyViolations(in:in:)``
|
||||
- ``ForeignKeyViolation``
|
||||
|
||||
### Sunsetted Methods
|
||||
|
||||
Those are legacy interfaces that are preserved for backwards compatibility. Their use is not recommended.
|
||||
|
||||
- ``Database/create(index:on:columns:unique:ifNotExists:condition:)``
|
||||
- ``Database/create(table:temporary:ifNotExists:withoutRowID:body:)``
|
||||
@@ -0,0 +1,256 @@
|
||||
# Sharing a Database
|
||||
|
||||
How to share an SQLite database between multiple processes • Recommendations for App Group containers, App Extensions, App Sandbox, and file coordination.
|
||||
|
||||
## Overview
|
||||
|
||||
**This guide describes a recommended setup that applies as soon as several processes want to access the same SQLite database.** It complements the <doc:Concurrency> guide, that you should read first.
|
||||
|
||||
On iOS for example, you can share database files between multiple processes by storing them in an [App Group Container](https://developer.apple.com/documentation/foundation/nsfilemanager/1412643-containerurlforsecurityapplicati). On macOS, several processes may want to open the same database, according to their particular sandboxing contexts.
|
||||
|
||||
Accessing a shared database from several SQLite connections, from several processes, creates challenges at various levels:
|
||||
|
||||
1. **Database setup** may be attempted by multiple processes, concurrently, with possible conflicts.
|
||||
2. **SQLite** may throw [`SQLITE_BUSY`] errors, "database is locked".
|
||||
3. **iOS** may kill your application with a [`0xDEAD10CC`] exception.
|
||||
4. **GRDB** <doc:DatabaseObservation> does not detect changes performed by external processes.
|
||||
|
||||
We'll address all of those challenges below.
|
||||
|
||||
> Important: Preventing errors that may happen due to database sharing is difficult. It is extremely difficult on iOS. And it is almost impossible to test.
|
||||
>
|
||||
> Always consider sharing plain files, or any other inter-process communication technique, before sharing an SQLite database.
|
||||
|
||||
## Use the WAL mode
|
||||
|
||||
In order to access a shared database, use a ``DatabasePool``. It opens the database in the [WAL mode], which helps sharing a database because it allows multiple processes to access the database concurrently.
|
||||
|
||||
It is also possible to use a ``DatabaseQueue``, with the `.wal` ``Configuration/journalMode``.
|
||||
|
||||
Since several processes may open the database at the same time, protect the creation of the database connection with an [NSFileCoordinator].
|
||||
|
||||
- In a process that can create and write in the database, use this sample code:
|
||||
|
||||
```swift
|
||||
/// Returns an initialized database pool at the shared location databaseURL
|
||||
func openSharedDatabase(at databaseURL: URL) throws -> DatabasePool {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
var coordinatorError: NSError?
|
||||
var dbPool: DatabasePool?
|
||||
var dbError: Error?
|
||||
coordinator.coordinate(writingItemAt: databaseURL, options: .forMerging, error: &coordinatorError) { url in
|
||||
do {
|
||||
dbPool = try openDatabase(at: url)
|
||||
} catch {
|
||||
dbError = error
|
||||
}
|
||||
}
|
||||
if let error = dbError ?? coordinatorError {
|
||||
throw error
|
||||
}
|
||||
return dbPool!
|
||||
}
|
||||
|
||||
private func openDatabase(at databaseURL: URL) throws -> DatabasePool {
|
||||
var configuration = Configuration()
|
||||
configuration.prepareDatabase { db in
|
||||
// Activate the persistent WAL mode so that
|
||||
// read-only processes can access the database.
|
||||
//
|
||||
// See https://www.sqlite.org/walformat.html#operations_that_require_locks_and_which_locks_those_operations_use
|
||||
// and https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpersistwal
|
||||
if db.configuration.readonly == false {
|
||||
var flag: CInt = 1
|
||||
let code = withUnsafeMutablePointer(to: &flag) { flagP in
|
||||
sqlite3_file_control(db.sqliteConnection, nil, SQLITE_FCNTL_PERSIST_WAL, flagP)
|
||||
}
|
||||
guard code == SQLITE_OK else {
|
||||
throw DatabaseError(resultCode: ResultCode(rawValue: code))
|
||||
}
|
||||
}
|
||||
}
|
||||
let dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration)
|
||||
|
||||
// Perform here other database setups, such as defining
|
||||
// the database schema with a DatabaseMigrator, and
|
||||
// checking if the application can open the file:
|
||||
try migrator.migrate(dbPool)
|
||||
if try dbPool.read(migrator.hasBeenSuperseded) {
|
||||
// Database is too recent
|
||||
throw /* some error */
|
||||
}
|
||||
|
||||
return dbPool
|
||||
}
|
||||
```
|
||||
|
||||
- In a process that only reads in the database, use this sample code:
|
||||
|
||||
```swift
|
||||
/// Returns an initialized database pool at the shared location databaseURL,
|
||||
/// or nil if the database is not created yet, or does not have the required
|
||||
/// schema version.
|
||||
func openSharedReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? {
|
||||
let coordinator = NSFileCoordinator(filePresenter: nil)
|
||||
var coordinatorError: NSError?
|
||||
var dbPool: DatabasePool?
|
||||
var dbError: Error?
|
||||
coordinator.coordinate(readingItemAt: databaseURL, options: .withoutChanges, error: &coordinatorError) { url in
|
||||
do {
|
||||
dbPool = try openReadOnlyDatabase(at: url)
|
||||
} catch {
|
||||
dbError = error
|
||||
}
|
||||
}
|
||||
if let error = dbError ?? coordinatorError {
|
||||
throw error
|
||||
}
|
||||
return dbPool
|
||||
}
|
||||
|
||||
private func openReadOnlyDatabase(at databaseURL: URL) throws -> DatabasePool? {
|
||||
do {
|
||||
var configuration = Configuration()
|
||||
configuration.readonly = true
|
||||
let dbPool = try DatabasePool(path: databaseURL.path, configuration: configuration)
|
||||
|
||||
// Check here if the database schema is the expected one,
|
||||
// for example with a DatabaseMigrator:
|
||||
return try dbPool.read { db in
|
||||
if try migrator.hasBeenSuperseded(db) {
|
||||
// Database is too recent
|
||||
return nil
|
||||
} else if try migrator.hasCompletedMigrations(db) == false {
|
||||
// Database is too old
|
||||
return nil
|
||||
}
|
||||
return dbPool
|
||||
}
|
||||
} catch {
|
||||
if FileManager.default.fileExists(atPath: databaseURL.path) {
|
||||
throw error
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### The Specific Case of Read-Only Connections
|
||||
|
||||
Read-only connections will fail unless two extra files ending in `-shm` and `-wal` are present next to the database file ([source](https://www.sqlite.org/walformat.html#operations_that_require_locks_and_which_locks_those_operations_use)). Those files are regular companions of databases in the [WAL mode]. But they are deleted, under regular operations, when database connections are closed. Precisely speaking, they *may* be deleted: it depends on the SQLite and the operating system versions ([source](https://github.com/groue/GRDB.swift/issues/739#issuecomment-604363998)). And when they are deleted, read-only connections fail.
|
||||
|
||||
The solution is to enable the "persistent WAL mode", as shown in the sample code above, by setting the [SQLITE_FCNTL_PERSIST_WAL](https://www.sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntlpersistwal) flag. This mode makes sure the `-shm` and `-wal` files are never deleted, and guarantees a database access to read-only connections.
|
||||
|
||||
|
||||
## How to limit the SQLITE_BUSY error
|
||||
|
||||
> SQLite Documentation: The [`SQLITE_BUSY`] result code indicates that the database file could not be written (or in some cases read) because of concurrent activity by some other database connection, usually a database connection in a separate process.
|
||||
|
||||
If several processes want to write in the database, configure the database pool of each process that wants to write:
|
||||
|
||||
```swift
|
||||
var configuration = Configuration()
|
||||
configuration.defaultTransactionKind = .immediate
|
||||
configuration.busyMode = .timeout(/* a TimeInterval */)
|
||||
let dbPool = try DatabasePool(path: ..., configuration: configuration)
|
||||
```
|
||||
|
||||
Both the `defaultTransactionKind` and `busyMode` are important for preventing `SQLITE_BUSY`. The `immediate` transaction kind prevents write transactions from overlapping, and the busy timeout has write transactions wait, instead of throwing `SQLITE_BUSY`, whenever another process is writing.
|
||||
|
||||
With such a setup, you will still get `SQLITE_BUSY` errors if the database remains locked by another process for longer than the specified timeout. You can catch those errors:
|
||||
|
||||
```swift
|
||||
do {
|
||||
try dbPool.write { db in ... }
|
||||
} catch DatabaseError.SQLITE_BUSY {
|
||||
// Another process won't let you write. Deal with it.
|
||||
}
|
||||
```
|
||||
|
||||
## How to limit the 0xDEAD10CC exception
|
||||
|
||||
> Apple documentation: [`0xDEAD10CC`] (pronounced “dead lock”): the operating system terminated the app because it held on to a file lock or SQLite database lock during suspension.
|
||||
|
||||
#### If you use SQLCipher
|
||||
|
||||
Use SQLCipher 4+, and configure the database from ``Configuration/prepareDatabase(_:)``:
|
||||
|
||||
```swift
|
||||
var configuration = Configuration()
|
||||
configuration.prepareDatabase { (db: Database) in
|
||||
try db.usePassphrase("secret")
|
||||
try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
|
||||
}
|
||||
let dbPool = try DatabasePool(path: ..., configuration: configuration)
|
||||
```
|
||||
|
||||
Applications become responsible for managing the salt themselves: see [instructions](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size). See also <https://github.com/sqlcipher/sqlcipher/issues/255> for more context and information.
|
||||
|
||||
#### In all cases
|
||||
|
||||
The technique described below is based on [this discussion](https://developer.apple.com/forums/thread/126438) on the Apple Developer Forums. It is [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features).
|
||||
|
||||
In each process that writes in the database, set the ``Configuration/observesSuspensionNotifications`` configuration flag:
|
||||
|
||||
```swift
|
||||
var configuration = Configuration()
|
||||
configuration.observesSuspensionNotifications = true
|
||||
let dbPool = try DatabasePool(path: ..., configuration: configuration)
|
||||
```
|
||||
|
||||
Post ``Database/suspendNotification`` when the application is about to be [suspended](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle). You can for example post this notification from `UIApplicationDelegate.applicationDidEnterBackground(_:)`, or in the expiration handler of a [background task](https://forums.developer.apple.com/thread/85066):
|
||||
|
||||
```swift
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Once suspended, a database won't acquire any new lock that could cause the `0xDEAD10CC` exception.
|
||||
|
||||
In exchange, you will get `SQLITE_INTERRUPT` (code 9) or `SQLITE_ABORT` (code 4) errors, with messages "Database is suspended", "Transaction was aborted", or "interrupted". You can catch those errors:
|
||||
|
||||
```swift
|
||||
do {
|
||||
try dbPool.write { db in ... }
|
||||
} catch DatabaseError.SQLITE_INTERRUPT, DatabaseError.SQLITE_ABORT {
|
||||
// Oops, the database is suspended.
|
||||
// Maybe try again after database is resumed?
|
||||
}
|
||||
```
|
||||
|
||||
Post ``Database/resumeNotification`` in order to resume suspended databases. You can safely post this notification when the app comes back to foreground.
|
||||
|
||||
In applications that use the background modes supported by iOS, post `resumeNotification` method from each and every background mode callback that may use the database, and don't forget to post `suspendNotification` again before the app turns suspended.
|
||||
|
||||
## How to perform cross-process database observation
|
||||
|
||||
<doc:DatabaseObservation> features are not able to detect database changes performed by other processes.
|
||||
|
||||
Whenever you need to notify other processes that the database has been changed, you will have to use a cross-process notification mechanism such as [NSFileCoordinator] or [CFNotificationCenterGetDarwinNotifyCenter]. You can trigger those notifications automatically with ``DatabaseRegionObservation``:
|
||||
|
||||
```swift
|
||||
// Notify all changes made to the database
|
||||
let observation = DatabaseRegionObservation(tracking: .fullDatabase)
|
||||
let observer = try observation.start(in: dbPool) { db in
|
||||
// Notify other processes
|
||||
}
|
||||
|
||||
// Notify changes made to the "player" and "team" tables only
|
||||
let observation = DatabaseRegionObservation(tracking: Player.all(), Team.all())
|
||||
let observer = try observation.start(in: dbPool) { db in
|
||||
// Notify other processes
|
||||
}
|
||||
```
|
||||
|
||||
The processes that observe the database can catch those notifications, and deal with the notified changes. See <doc:GRDB/TransactionObserver#Dealing-with-Undetected-Changes> for some related techniques.
|
||||
|
||||
[NSFileCoordinator]: https://developer.apple.com/documentation/foundation/nsfilecoordinator
|
||||
[CFNotificationCenterGetDarwinNotifyCenter]: https://developer.apple.com/documentation/corefoundation/1542572-cfnotificationcentergetdarwinnot
|
||||
[WAL mode]: https://www.sqlite.org/wal.html
|
||||
[`SQLITE_BUSY`]: https://www.sqlite.org/rescode.html#busy
|
||||
[`0xDEAD10CC`]: https://developer.apple.com/documentation/xcode/understanding-the-exception-types-in-a-crash-report
|
||||
@@ -0,0 +1,109 @@
|
||||
# ``GRDB/Configuration``
|
||||
|
||||
The configuration of a database connection.
|
||||
|
||||
## Overview
|
||||
|
||||
You create a `Configuration` before opening a database connection:
|
||||
|
||||
```swift
|
||||
var config = Configuration()
|
||||
config.readonly = true
|
||||
config.maximumReaderCount = 2 // (DatabasePool only) The default is 5
|
||||
|
||||
let dbQueue = try DatabaseQueue( // or DatabasePool
|
||||
path: "/path/to/database.sqlite",
|
||||
configuration: config)
|
||||
```
|
||||
|
||||
See <doc:DatabaseConnections>.
|
||||
|
||||
## Frequent Use Cases
|
||||
|
||||
#### Tracing SQL Statements
|
||||
|
||||
You can setup a tracing function that prints out all executed SQL requests with ``prepareDatabase(_:)`` and ``Database/trace(options:_:)``:
|
||||
|
||||
```swift
|
||||
var config = Configuration()
|
||||
config.prepareDatabase { db in
|
||||
db.trace { print("SQL> \($0)") }
|
||||
}
|
||||
|
||||
let dbQueue = try DatabaseQueue(
|
||||
path: "/path/to/database.sqlite",
|
||||
configuration: config)
|
||||
|
||||
// Prints "SQL> SELECT COUNT(*) FROM player"
|
||||
let playerCount = dbQueue.read { db in
|
||||
try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
#### Public Statement Arguments
|
||||
|
||||
Debugging is easier when database errors and tracing functions expose the values sent to the database. Since those values may contain sensitive information, verbose logging is disabled by default. You turn it on with ``publicStatementArguments``:
|
||||
|
||||
```swift
|
||||
var config = Configuration()
|
||||
#if DEBUG
|
||||
// Protect sensitive information by enabling
|
||||
// verbose debugging in DEBUG builds only.
|
||||
config.publicStatementArguments = true
|
||||
#endif
|
||||
|
||||
let dbQueue = try DatabaseQueue(
|
||||
path: "/path/to/database.sqlite",
|
||||
configuration: config)
|
||||
|
||||
do {
|
||||
try dbQueue.write { db in
|
||||
user.name = ...
|
||||
user.location = ...
|
||||
user.address = ...
|
||||
user.phoneNumber = ...
|
||||
try user.save(db)
|
||||
}
|
||||
} catch {
|
||||
// Prints sensitive information in debug builds only
|
||||
print(error)
|
||||
}
|
||||
```
|
||||
|
||||
> Warning: It is your responsibility to prevent sensitive information from leaking in unexpected locations, so you should not set the `publicStatementArguments` flag in release builds (think about GDPR and other privacy-related rules).
|
||||
|
||||
## Topics
|
||||
|
||||
### Creating a Configuration
|
||||
|
||||
- ``init()``
|
||||
|
||||
### Configuring SQLite Connections
|
||||
|
||||
- ``acceptsDoubleQuotedStringLiterals``
|
||||
- ``busyMode``
|
||||
- ``foreignKeysEnabled``
|
||||
- ``journalMode``
|
||||
- ``readonly``
|
||||
- ``JournalModeConfiguration``
|
||||
|
||||
### Configuring GRDB Connections
|
||||
|
||||
- ``allowsUnsafeTransactions``
|
||||
- ``defaultTransactionKind``
|
||||
- ``label``
|
||||
- ``maximumReaderCount``
|
||||
- ``observesSuspensionNotifications``
|
||||
- ``persistentReadOnlyConnections``
|
||||
- ``prepareDatabase(_:)``
|
||||
- ``publicStatementArguments``
|
||||
- ``transactionClock``
|
||||
- ``TransactionClock``
|
||||
|
||||
### Configuring the Quality of Service
|
||||
|
||||
- ``qos``
|
||||
- ``readQoS``
|
||||
- ``writeQoS``
|
||||
- ``targetQueue``
|
||||
- ``writeTargetQueue``
|
||||
@@ -0,0 +1,95 @@
|
||||
# ``GRDB/DatabasePool``
|
||||
|
||||
A database connection that allows concurrent accesses to an SQLite database.
|
||||
|
||||
## Usage
|
||||
|
||||
Open a `DatabasePool` with the path to a database file:
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
|
||||
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
||||
```
|
||||
|
||||
SQLite creates the database file if it does not already exist. The connection is closed when the database queue gets deallocated.
|
||||
|
||||
**A `DatabasePool` can be used from any thread.** The ``DatabaseWriter/write(_:)-76inz`` and ``DatabaseReader/read(_:)-3806d`` methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue:
|
||||
|
||||
```swift
|
||||
// Modify the database:
|
||||
try dbPool.write { db in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
}
|
||||
|
||||
// Read values:
|
||||
try dbPool.read { db in
|
||||
let players = try Player.fetchAll(db)
|
||||
let playerCount = try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
Database access methods can return values:
|
||||
|
||||
```swift
|
||||
let playerCount = try dbPool.read { db in
|
||||
try Place.fetchCount(db)
|
||||
}
|
||||
|
||||
let newPlayerCount = try dbPool.write { db -> Int in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
The ``DatabaseWriter/write(_:)-76inz`` method wraps your database statements in a transaction that commits if and only if no error occurs. On the first unhandled error, all changes are reverted, the whole transaction is rollbacked, and the error is rethrown.
|
||||
|
||||
When you don't need to modify the database, prefer the ``DatabaseReader/read(_:)-3806d`` method, because several threads can perform reads in parallel.
|
||||
|
||||
When precise transaction handling is required, see <doc:Transactions>.
|
||||
|
||||
Asynchronous database accesses are described in <doc:Concurrency>.
|
||||
|
||||
`DatabasePool` can take snapshots of the database: see ``DatabaseSnapshot`` and ``DatabaseSnapshotPool``.
|
||||
|
||||
`DatabasePool` can be configured with ``Configuration``.
|
||||
|
||||
## Concurrency
|
||||
|
||||
A `DatabasePool` creates one writer SQLite connection, and a pool of read-only SQLite connections.
|
||||
|
||||
Unless ``Configuration/readonly``, the database is set to the [WAL mode](https://sqlite.org/wal.html). The WAL mode makes it possible for reads and writes to proceed concurrently.
|
||||
|
||||
All write accesses are executed in a serial **writer dispatch queue**, which means that there is never more than one thread that writes in the database.
|
||||
|
||||
All read accesses are executed in **reader dispatch queues** (one per read-only SQLite connection). Reads are generally non-blocking, unless the maximum number of concurrent reads has been reached. In this case, a read has to wait for another read to complete. That maximum number can be configured with ``Configuration/maximumReaderCount``.
|
||||
|
||||
SQLite connections are closed when the `DatabasePool` is deallocated.
|
||||
|
||||
`DatabasePool` inherits most of its database access methods from the ``DatabaseReader`` and ``DatabaseWriter`` protocols. It defines a few specific database access methods as well, listed below.
|
||||
|
||||
A `DatabasePool` needs your application to follow rules in order to deliver its safety guarantees. See <doc:Concurrency> for more information.
|
||||
|
||||
## Topics
|
||||
|
||||
### Creating a DatabasePool
|
||||
|
||||
- ``init(path:configuration:)``
|
||||
|
||||
### Accessing the Database
|
||||
|
||||
See ``DatabaseReader`` and ``DatabaseWriter`` for more database access methods.
|
||||
|
||||
- ``asyncConcurrentRead(_:)``
|
||||
- ``writeInTransaction(_:_:)``
|
||||
|
||||
### Creating Database Snapshots
|
||||
|
||||
- ``makeSnapshot()``
|
||||
- ``makeSnapshotPool()``
|
||||
|
||||
### Managing SQLite Connections
|
||||
|
||||
- ``invalidateReadOnlyConnections()``
|
||||
- ``releaseMemory()``
|
||||
- ``releaseMemoryEventually()``
|
||||
@@ -0,0 +1,103 @@
|
||||
# ``GRDB/DatabaseQueue``
|
||||
|
||||
A database connection that serializes accesses to an SQLite database.
|
||||
|
||||
## Usage
|
||||
|
||||
Open a `DatabaseQueue` with the path to a database file:
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
|
||||
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
|
||||
```
|
||||
|
||||
SQLite creates the database file if it does not already exist. The connection is closed when the database queue gets deallocated.
|
||||
|
||||
**A `DatabaseQueue` can be used from any thread.** The ``DatabaseWriter/write(_:)-76inz`` and ``DatabaseReader/read(_:)-3806d`` methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue:
|
||||
|
||||
```swift
|
||||
// Modify the database:
|
||||
try dbQueue.write { db in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
}
|
||||
|
||||
// Read values:
|
||||
try dbQueue.read { db in
|
||||
let players = try Player.fetchAll(db)
|
||||
let playerCount = try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
Database access methods can return values:
|
||||
|
||||
```swift
|
||||
let playerCount = try dbQueue.read { db in
|
||||
try Place.fetchCount(db)
|
||||
}
|
||||
|
||||
let newPlayerCount = try dbQueue.write { db -> Int in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
return try Player.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
The ``DatabaseWriter/write(_:)-76inz`` method wraps your database statements in a transaction that commits if and only if no error occurs. On the first unhandled error, all changes are reverted, the whole transaction is rollbacked, and the error is rethrown.
|
||||
|
||||
When you don't need to modify the database, prefer the ``DatabaseReader/read(_:)-3806d`` method: it prevents any modification to the database.
|
||||
|
||||
When precise transaction handling is required, see <doc:Transactions>.
|
||||
|
||||
Asynchronous database accesses are described in <doc:Concurrency>.
|
||||
|
||||
`DatabaseQueue` can be configured with ``Configuration``.
|
||||
|
||||
## In-Memory Databases
|
||||
|
||||
`DatabaseQueue` can open a connection to an [in-memory SQLite database](https://www.sqlite.org/inmemorydb.html).
|
||||
|
||||
Such connections are quite handy for tests and SwiftUI previews, since you do not have to perform any cleanup of the file system.
|
||||
|
||||
```swift
|
||||
let dbQueue = try DatabaseQueue()
|
||||
```
|
||||
|
||||
In order to create several connections to the same in-memory database, give this database a name:
|
||||
|
||||
```swift
|
||||
// A shared in-memory database
|
||||
let dbQueue1 = try DatabaseQueue(named: "myDatabase")
|
||||
|
||||
// Another connection to the same database
|
||||
let dbQueue2 = try DatabaseQueue(named: "myDatabase")
|
||||
```
|
||||
|
||||
See ``init(named:configuration:)``.
|
||||
|
||||
## Concurrency
|
||||
|
||||
A `DatabaseQueue` creates one single SQLite connection. All database accesses are executed in a serial **writer dispatch queue**, which means that there is never more than one thread that uses the database. The SQLite connection is closed when the `DatabaseQueue` is deallocated.
|
||||
|
||||
`DatabaseQueue` inherits most of its database access methods from the ``DatabaseReader`` and ``DatabaseWriter`` protocols. It defines a few specific database access methods as well, listed below.
|
||||
|
||||
A `DatabaseQueue` needs your application to follow rules in order to deliver its safety guarantees. See <doc:Concurrency> for more information.
|
||||
|
||||
## Topics
|
||||
|
||||
### Creating a DatabaseQueue
|
||||
|
||||
- ``init(named:configuration:)``
|
||||
- ``init(path:configuration:)``
|
||||
- ``inMemoryCopy(fromPath:configuration:)``
|
||||
- ``temporaryCopy(fromPath:configuration:)``
|
||||
|
||||
### Accessing the Database
|
||||
|
||||
See ``DatabaseReader`` and ``DatabaseWriter`` for more database access methods.
|
||||
|
||||
- ``inDatabase(_:)``
|
||||
- ``inTransaction(_:_:)``
|
||||
|
||||
### Managing the SQLite Connection
|
||||
|
||||
- ``releaseMemory()``
|
||||
@@ -0,0 +1,111 @@
|
||||
# ``GRDB/DatabaseRegionObservation``
|
||||
|
||||
`DatabaseRegionObservation` tracks changes in a database region, and notifies impactful transactions.
|
||||
|
||||
## Overview
|
||||
|
||||
`DatabaseRegionObservation` tracks insertions, updates, and deletions that impact the tracked region, whether performed with raw SQL, or <doc:QueryInterface>. This includes indirect changes triggered by [foreign keys actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
|
||||
|
||||
See <doc:GRDB/DatabaseRegionObservation#Dealing-with-Undetected-Changes> below for the list of exceptions.
|
||||
|
||||
`DatabaseRegionObservation` calls your application right after changes have been committed in the database, and before any other thread had any opportunity to perform further changes. *This is a pretty strong guarantee, that most applications do not really need.* Instead, most applications prefer to be notified with fresh values: make sure you check ``ValueObservation`` before using `DatabaseRegionObservation`.
|
||||
|
||||
## DatabaseRegionObservation Usage
|
||||
|
||||
Create a `DatabaseRegionObservation` with one or several requests to track:
|
||||
|
||||
```swift
|
||||
// Tracks the full player table
|
||||
let observation = DatabaseRegionObservation(tracking: Player.all())
|
||||
```
|
||||
|
||||
Then start the observation from a ``DatabaseQueue`` or ``DatabasePool``:
|
||||
|
||||
```swift
|
||||
let cancellable = try observation.start(in: dbQueue) { error in
|
||||
// Handle error
|
||||
} onChange: { (db: Database) in
|
||||
print("Players were changed")
|
||||
}
|
||||
```
|
||||
|
||||
Enjoy the changes notifications:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
try Player(name: "Arthur").insert(db)
|
||||
}
|
||||
// Prints "Players were changed"
|
||||
```
|
||||
|
||||
You stop the observation by calling the ``DatabaseCancellable/cancel()`` method on the object returned by the `start` method. Cancellation is automatic when the cancellable is deallocated:
|
||||
|
||||
```swift
|
||||
cancellable.cancel()
|
||||
```
|
||||
|
||||
`DatabaseRegionObservation` can also be turned into a Combine publisher, or an RxSwift observable (see the companion library [RxGRDB](https://github.com/RxSwiftCommunity/RxGRDB)):
|
||||
|
||||
```swift
|
||||
let cancellable = observation.publisher(in: dbQueue).sink { completion in
|
||||
// Handle completion
|
||||
} receiveValue: { (db: Database) in
|
||||
print("Players were changed")
|
||||
}
|
||||
```
|
||||
|
||||
You can feed `DatabaseRegionObservation` with any type that conforms to the ``DatabaseRegionConvertible`` protocol: ``FetchRequest``, ``DatabaseRegion``, ``Table``, etc. For example:
|
||||
|
||||
```swift
|
||||
// Observe the score column of the 'player' table
|
||||
let observation = DatabaseRegionObservation(
|
||||
tracking: Player.select(Column("score")))
|
||||
|
||||
// Observe the 'score' column of the 'player' table
|
||||
let observation = DatabaseRegionObservation(
|
||||
tracking: SQLRequest("SELECT score FROM player"))
|
||||
|
||||
// Observe both the 'player' and 'team' tables
|
||||
let observation = DatabaseRegionObservation(
|
||||
tracking: Table("player"), Table("team"))
|
||||
|
||||
// Observe the full database
|
||||
let observation = DatabaseRegionObservation(
|
||||
tracking: .fullDatabase)
|
||||
```
|
||||
|
||||
## Dealing with Undetected Changes
|
||||
|
||||
`DatabaseRegionObservation` will not notify impactful transactions whenever the database is modified in an undetectable way:
|
||||
|
||||
- Changes performed by external database connections.
|
||||
- Changes performed by SQLite statements that are not compiled and executed by GRDB.
|
||||
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
|
||||
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
|
||||
|
||||
To have observations notify such undetected changes, applications can take explicit action: call the ``Database/notifyChanges(in:)`` `Database` method from a write transaction:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Notify observations that some changes were performed in the database
|
||||
try db.notifyChanges(in: .fullDatabase)
|
||||
|
||||
// Notify observations that some changes were performed in the player table
|
||||
try db.notifyChanges(in: Player.all())
|
||||
|
||||
// Equivalent alternative
|
||||
try db.notifyChanges(in: Table("player"))
|
||||
}
|
||||
```
|
||||
|
||||
## Topics
|
||||
|
||||
### Creating DatabaseRegionObservation
|
||||
|
||||
- ``init(tracking:)-5ldbe``
|
||||
- ``init(tracking:)-2nqjd``
|
||||
|
||||
### Observing Database Transactions
|
||||
|
||||
- ``publisher(in:)``
|
||||
- ``start(in:onError:onChange:)``
|
||||
@@ -0,0 +1,183 @@
|
||||
# ``GRDB/DatabaseValueConvertible``
|
||||
|
||||
A type that can convert itself into and out of a database value.
|
||||
|
||||
## Overview
|
||||
|
||||
A `DatabaseValueConvertible` type supports conversion to and from database values (null, integers, doubles, strings, and blobs). `DatabaseValueConvertible` is adopted by `Bool`, `Int`, `String`, `Date`, etc.
|
||||
|
||||
> Note: Types that converts to and from multiple columns in a database row must not conform to the `DatabaseValueConvertible` protocol. Those types are called **record types**, and should conform to record protocols instead. See <doc:QueryInterface>.
|
||||
|
||||
> Note: Standard collections `Array`, `Set`, and `Dictionary` do not conform to `DatabaseValueConvertible`. To store arrays, sets, or dictionaries in individual database values, wrap them as properties of `Codable` record types. They will automatically be stored as JSON objects and arrays. See <doc:QueryInterface>.
|
||||
|
||||
## Conforming to the DatabaseValueConvertible Protocol
|
||||
|
||||
To conform to `DatabaseValueConvertible`, implement the two requirements ``fromDatabaseValue(_:)-21zzv`` and ``databaseValue-1ob9k``. Do not customize the ``fromMissingColumn()-7iamp`` requirement. If your type `MyValue` conforms, then the conformance of the optional type `MyValue?` is automatic.
|
||||
|
||||
The implementation of `fromDatabaseValue` must return nil if the type can not be decoded from the raw database value. This nil value will have GRDB throw a decoding error accordingly.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
struct EvenInteger {
|
||||
let value: Int // Guaranteed even
|
||||
|
||||
init?(_ value: Int) {
|
||||
guard value.isMultiple(of: 2) else {
|
||||
return nil // Not an even number
|
||||
}
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension EvenInteger: DatabaseValueConvertible {
|
||||
var databaseValue: DatabaseValue {
|
||||
value.databaseValue
|
||||
}
|
||||
|
||||
static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
|
||||
guard let value = Int.fromDatabaseValue(dbValue) else {
|
||||
return nil // Not an integer
|
||||
}
|
||||
return EvenInteger(value) // Nil if not even
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Built-in RawRepresentable support
|
||||
|
||||
`DatabaseValueConvertible` implementation is ready-made for `RawRepresentable` types whose raw value is itself `DatabaseValueConvertible`, such as enums:
|
||||
|
||||
```swift
|
||||
enum Grape: String {
|
||||
case chardonnay, merlot, riesling
|
||||
}
|
||||
|
||||
// Encodes and decodes `Grape` as a string in the database:
|
||||
extension Grape: DatabaseValueConvertible { }
|
||||
```
|
||||
|
||||
### Built-in Codable support
|
||||
|
||||
`DatabaseValueConvertible` is also ready-made for `Codable` types, which are automatically coded and decoded from JSON arrays and objects:
|
||||
|
||||
```swift
|
||||
struct Color: Codable {
|
||||
var red: Double
|
||||
var green: Double
|
||||
var blue: Double
|
||||
}
|
||||
|
||||
// Encodes and decodes `Color` as a JSON object in the database:
|
||||
extension Color: DatabaseValueConvertible { }
|
||||
```
|
||||
|
||||
By default, such codable value types are encoded and decoded with the standard [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) and [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder). `Data` values are handled with the `.base64` strategy, `Date` with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy.
|
||||
|
||||
To customize the JSON format, provide an explicit implementation for the `DatabaseValueConvertible` requirements, or implement these two methods:
|
||||
|
||||
```swift
|
||||
protocol DatabaseValueConvertible {
|
||||
static func databaseJSONDecoder() -> JSONDecoder
|
||||
static func databaseJSONEncoder() -> JSONEncoder
|
||||
}
|
||||
```
|
||||
|
||||
### Adding support for the Tagged library
|
||||
|
||||
[Tagged](https://github.com/pointfreeco/swift-tagged) is a popular library that makes it possible to enhance the type-safety of our programs with dedicated wrappers around basic types. For example:
|
||||
|
||||
```swift
|
||||
import Tagged
|
||||
|
||||
struct Player: Identifiable {
|
||||
// Thanks to Tagged, Player.ID can not be mismatched with Team.ID or
|
||||
// Award.ID, even though they all wrap strings.
|
||||
typealias ID = Tagged<Player, String>
|
||||
var id: ID
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
```
|
||||
|
||||
Applications that use both Tagged and GRDB will want to add those lines somewhere:
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
import Tagged
|
||||
|
||||
// Add database support to Tagged values
|
||||
extension Tagged: SQLExpressible where RawValue: SQLExpressible { }
|
||||
extension Tagged: StatementBinding where RawValue: StatementBinding { }
|
||||
extension Tagged: StatementColumnConvertible where RawValue: StatementColumnConvertible { }
|
||||
extension Tagged: DatabaseValueConvertible where RawValue: DatabaseValueConvertible { }
|
||||
```
|
||||
|
||||
This makes it possible to use `Tagged` values in all the expected places:
|
||||
|
||||
```swift
|
||||
let id: Player.ID = ...
|
||||
let player = try Player.find(db, id: id)
|
||||
```
|
||||
|
||||
## Optimized Values
|
||||
|
||||
For extra performance, custom value types can conform to both `DatabaseValueConvertible` and ``StatementColumnConvertible``. This extra protocol grants raw access to the [low-level C SQLite interface](https://www.sqlite.org/c3ref/column_blob.html) when decoding values.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
extension EvenInteger: StatementColumnConvertible {
|
||||
init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let value = Int(exactly: int64) else {
|
||||
return nil // Does not fit Int (probably a 32-bit architecture)
|
||||
}
|
||||
self.init(value) // Nil if not even
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This extra conformance is not required: only aim at the low-level C interface if you have identified a performance issue after profiling your application!
|
||||
|
||||
## Topics
|
||||
|
||||
### Creating a Value
|
||||
|
||||
- ``fromDatabaseValue(_:)-21zzv``
|
||||
- ``fromMissingColumn()-7iamp``
|
||||
|
||||
### Accessing the DatabaseValue
|
||||
|
||||
- ``databaseValue-1ob9k``
|
||||
|
||||
### Configuring the JSON format for the standard Decodable protocol
|
||||
|
||||
- ``databaseJSONDecoder()-7zou9``
|
||||
- ``databaseJSONEncoder()-37sff``
|
||||
|
||||
### Fetching Values from Raw SQL
|
||||
|
||||
- ``fetchCursor(_:sql:arguments:adapter:)-6elcz``
|
||||
- ``fetchAll(_:sql:arguments:adapter:)-1cqyb``
|
||||
- ``fetchSet(_:sql:arguments:adapter:)-5jene``
|
||||
- ``fetchOne(_:sql:arguments:adapter:)-qvqp``
|
||||
|
||||
### Fetching Values from a Prepared Statement
|
||||
|
||||
- ``fetchCursor(_:arguments:adapter:)-4l6af``
|
||||
- ``fetchAll(_:arguments:adapter:)-3abuc``
|
||||
- ``fetchSet(_:arguments:adapter:)-6y54n``
|
||||
- ``fetchOne(_:arguments:adapter:)-3d7ax``
|
||||
|
||||
### Fetching Values from a Request
|
||||
|
||||
- ``fetchCursor(_:_:)-8q4r6``
|
||||
- ``fetchAll(_:_:)-9hkqs``
|
||||
- ``fetchSet(_:_:)-1foke``
|
||||
- ``fetchOne(_:_:)-o6yj``
|
||||
|
||||
### Supporting Types
|
||||
|
||||
- ``DatabaseValueCursor``
|
||||
- ``StatementBinding``
|
||||
@@ -0,0 +1,205 @@
|
||||
# ``GRDB/Statement``
|
||||
|
||||
A prepared statement.
|
||||
|
||||
## Overview
|
||||
|
||||
Prepared statements let you execute an SQL query several times, with different arguments if needed.
|
||||
|
||||
Reusing prepared statements is a performance optimization technique because SQLite parses and analyses the SQL query only once, when the prepared statement is created.
|
||||
|
||||
## Building Prepared Statements
|
||||
|
||||
Build a prepared statement with the ``Database/makeStatement(sql:)`` method:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
let insertStatement = try db.makeStatement(sql: """
|
||||
INSERT INTO player (name, score) VALUES (:name, :score)
|
||||
""")
|
||||
|
||||
let selectStatement = try db.makeStatement(sql: """
|
||||
SELECT * FROM player WHERE name = ?
|
||||
""")
|
||||
}
|
||||
```
|
||||
|
||||
The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. Set the values for those arguments with arrays or dictionaries of database values, or ``StatementArguments`` instances:
|
||||
|
||||
```swift
|
||||
insertStatement.arguments = ["name": "Arthur", "score": 1000]
|
||||
selectStatement.arguments = ["Arthur"]
|
||||
```
|
||||
|
||||
Alternatively, the ``Database/makeStatement(literal:)`` method creates prepared statements with support for [SQL Interpolation]:
|
||||
|
||||
```swift
|
||||
let insertStatement = try db.makeStatement(literal: "INSERT ...")
|
||||
let selectStatement = try db.makeStatement(literal: "SELECT ...")
|
||||
// ~~~~~~~
|
||||
```
|
||||
|
||||
The `makeStatement` methods throw an error of code `SQLITE_MISUSE` (21) if the SQL query contains multiple statements joined with a semicolon. See <doc:GRDB/Statement#Parsing-Multiple-Prepared-Statements-from-a-Single-SQL-String> below.
|
||||
|
||||
## Executing Prepared Statements and Fetching Values
|
||||
|
||||
Prepared statements can be executed:
|
||||
|
||||
```swift
|
||||
try insertStatement.execute()
|
||||
```
|
||||
|
||||
To fetch rows and values from a prepared statement, use a fetching method of ``Row``, ``DatabaseValueConvertible``, or ``FetchableRecord``:
|
||||
|
||||
```swift
|
||||
let players = try Player.fetchCursor(selectStatement) // A Cursor of Player
|
||||
let players = try Player.fetchAll(selectStatement) // [Player]
|
||||
let players = try Player.fetchSet(selectStatement) // Set<Player>
|
||||
let player = try Player.fetchOne(selectStatement) // Player?
|
||||
// ~~~~~~ or Row, Int, String, Date, etc.
|
||||
```
|
||||
|
||||
Arguments can be set at the moment of the statement execution:
|
||||
|
||||
```swift
|
||||
try insertStatement.execute(arguments: ["name": "Arthur", "score": 1000])
|
||||
let player = try Player.fetchOne(selectStatement, arguments: ["Arthur"])
|
||||
```
|
||||
|
||||
> Note: A prepared statement that has failed with an error can not be recovered. Create a new instance, or use a cached statement as described below.
|
||||
|
||||
> Tip: When you look after the best performance, take care about a difference between setting the arguments before execution, and setting the arguments at the moment of execution:
|
||||
>
|
||||
> ```swift
|
||||
> // First option
|
||||
> try statement.setArguments(...)
|
||||
> try statement.execute()
|
||||
>
|
||||
> // Second option
|
||||
> try statement.execute(arguments: ...)
|
||||
> ```
|
||||
>
|
||||
> Both perform exactly the same action, and most applications should not care about the difference. Yet:
|
||||
>
|
||||
> - ``setArguments(_:)`` performs a copy of string and blob arguments. It uses the low-level [`SQLITE_TRANSIENT`](https://www.sqlite.org/c3ref/c_static.html) option, and fits well the reuse of a given statement with the same arguments.
|
||||
> - ``execute(arguments:)`` avoids a temporary allocation for string and blob arguments if the number of arguments is small. Instead of `SQLITE_TRANSIENT`, it uses the low-level [`SQLITE_STATIC`](https://www.sqlite.org/c3ref/c_static.html) option. This fits well the reuse of a given statement with various arguments.
|
||||
>
|
||||
> Don't make a blind choice, and monitor your app performance if it really matters!
|
||||
|
||||
## Caching Prepared Statements
|
||||
|
||||
When the same query will be used several times in the lifetime of an application, one may feel a natural desire to cache prepared statements.
|
||||
|
||||
Don't cache statements yourself.
|
||||
|
||||
> Note: This is because an application lacks the necessary tools. Statements are tied to specific SQLite connections and dispatch queues which are not managed by the application, especially with a ``DatabasePool`` connection. A change in the database schema [may, or may not](https://www.sqlite.org/compile.html#max_schema_retry) invalidate a statement.
|
||||
|
||||
Instead, use the ``Database/cachedStatement(sql:)`` method. GRDB does all the hard caching and memory management:
|
||||
|
||||
```swift
|
||||
let statement = try db.cachedStatement(sql: "INSERT ...")
|
||||
```
|
||||
|
||||
The variant ``Database/cachedStatement(literal:)`` supports [SQL Interpolation]:
|
||||
|
||||
```swift
|
||||
let statement = try db.cachedStatement(literal: "INSERT ...")
|
||||
```
|
||||
|
||||
Should a cached prepared statement throw an error, don't reuse it. Instead, reload one from the cache.
|
||||
|
||||
## Parsing Multiple Prepared Statements from a Single SQL String
|
||||
|
||||
To build multiple statements joined with a semicolon, use ``Database/allStatements(sql:arguments:)``:
|
||||
|
||||
```swift
|
||||
let statements = try db.allStatements(sql: """
|
||||
INSERT INTO player (name, score) VALUES (?, ?);
|
||||
INSERT INTO player (name, score) VALUES (?, ?);
|
||||
""", arguments: ["Arthur", 100, "O'Brien", 1000])
|
||||
while let statement = try statements.next() {
|
||||
try statement.execute()
|
||||
}
|
||||
```
|
||||
|
||||
The variant ``Database/allStatements(literal:)`` supports [SQL Interpolation]:
|
||||
|
||||
```swift
|
||||
let statements = try db.allStatements(literal: """
|
||||
INSERT INTO player (name, score) VALUES (\("Arthur"), \(100));
|
||||
INSERT INTO player (name, score) VALUES (\("O'Brien"), \(1000));
|
||||
""")
|
||||
// An alternative way to iterate all statements
|
||||
try statements.forEach { statement in
|
||||
try statement.execute()
|
||||
}
|
||||
```
|
||||
|
||||
> Tip: When you intend to run all statements in an SQL string but don't care about individual ones, don't bother iterating individual statement instances! Skip this documentation section and just use ``Database/execute(sql:arguments:)``:
|
||||
>
|
||||
> ```swift
|
||||
> try db.execute(sql: """
|
||||
> CREATE TABLE player ...;
|
||||
> INSERT INTO player ...;
|
||||
> """)
|
||||
> ```
|
||||
|
||||
The results of multiple `SELECT` statements can be joined into a single ``Cursor``. This is the GRDB version of the [`sqlite3_exec()`](https://www.sqlite.org/c3ref/exec.html) function:
|
||||
|
||||
```swift
|
||||
let statements = try db.allStatements(sql: """
|
||||
SELECT ...;
|
||||
SELECT ...;
|
||||
""")
|
||||
let players = try statements.flatMap { statement in
|
||||
try Player.fetchCursor(statement)
|
||||
}
|
||||
for let player = try players.next() {
|
||||
print(player.name)
|
||||
}
|
||||
```
|
||||
|
||||
The ``SQLStatementCursor`` returned from `allStatements` can be turned into a regular Swift array, but in this case make sure all individual statements can compile even if the previous ones were not executed:
|
||||
|
||||
```swift
|
||||
// OK: Array of statements
|
||||
let statements = try Array(db.allStatements(sql: """
|
||||
INSERT ...;
|
||||
UPDATE ...;
|
||||
"""))
|
||||
|
||||
// FAILURE: Can't build an array of statements since the INSERT won't
|
||||
// compile until CREATE TABLE is executed.
|
||||
let statements = try Array(db.allStatements(sql: """
|
||||
CREATE TABLE player ...;
|
||||
INSERT INTO player ...;
|
||||
"""))
|
||||
```
|
||||
|
||||
## Topics
|
||||
|
||||
### Executing a Prepared Statement
|
||||
|
||||
- ``execute(arguments:)``
|
||||
|
||||
### Arguments
|
||||
|
||||
- ``arguments``
|
||||
- ``setArguments(_:)``
|
||||
- ``setUncheckedArguments(_:)``
|
||||
- ``validateArguments(_:)``
|
||||
- ``StatementArguments``
|
||||
|
||||
### Statement Informations
|
||||
|
||||
- ``columnCount``
|
||||
- ``columnNames``
|
||||
- ``databaseRegion``
|
||||
- ``index(ofColumn:)``
|
||||
- ``isReadonly``
|
||||
- ``sql``
|
||||
- ``sqliteStatement``
|
||||
- ``SQLiteStatement``
|
||||
|
||||
|
||||
[SQL Interpolation]: https://github.com/groue/GRDB.swift/blob/master/Documentation/SQLInterpolation.md
|
||||
@@ -0,0 +1,285 @@
|
||||
# ``GRDB/TransactionObserver``
|
||||
|
||||
A type that tracks database changes and transactions performed in a database.
|
||||
|
||||
## Overview
|
||||
|
||||
`TransactionObserver` is the low-level protocol that supports all <doc:DatabaseObservation> features.
|
||||
|
||||
A transaction observer is notified of individual changes (inserts, updates and deletes), before they are committed to disk, as well as transaction commits and rollbacks.
|
||||
|
||||
## Activate a Transaction Observer
|
||||
|
||||
An observer starts receiving change notifications after it has been added to a database connection with the ``DatabaseWriter/add(transactionObserver:extent:)`` `DatabaseWriter` method, or the ``Database/add(transactionObserver:extent:)`` `Database` method:
|
||||
|
||||
```swift
|
||||
let observer = MyObserver()
|
||||
dbQueue.add(transactionObserver: observer)
|
||||
```
|
||||
|
||||
By default, database holds weak references to its transaction observers: they are not retained, and stop getting notifications after they are deallocated. See <doc:TransactionObserver#Observation-Extent> for more options.
|
||||
|
||||
## Database Changes And Transactions
|
||||
|
||||
Database changes are notified to the ``databaseDidChange(with:)`` callback. This includes indirect changes triggered by `ON DELETE` and `ON UPDATE` actions associated to [foreign keys](https://www.sqlite.org/foreignkeys.html#fk_actions), and [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
|
||||
|
||||
Transaction completions are notified to the ``databaseWillCommit()-7mksu``, ``databaseDidCommit(_:)`` and ``databaseDidRollback(_:)`` callbacks.
|
||||
|
||||
> Important: Some changes and transactions are not automatically notified. See <doc:GRDB/TransactionObserver#Dealing-with-Undetected-Changes> below.
|
||||
|
||||
Notified changes are not actually written to disk until the transaction commits, and the `databaseDidCommit` callback is called. On the other side, `databaseDidRollback` confirms their invalidation:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
try db.execute(sql: "INSERT ...") // 1. didChange
|
||||
try db.execute(sql: "UPDATE ...") // 2. didChange
|
||||
} // 3. willCommit, 4. didCommit
|
||||
|
||||
try dbQueue.inTransaction { db in
|
||||
try db.execute(sql: "INSERT ...") // 1. didChange
|
||||
try db.execute(sql: "UPDATE ...") // 2. didChange
|
||||
return .rollback // 3. didRollback
|
||||
}
|
||||
|
||||
try dbQueue.write { db in
|
||||
try db.execute(sql: "INSERT ...") // 1. didChange
|
||||
throw SomeError()
|
||||
} // 2. didRollback
|
||||
```
|
||||
|
||||
Database statements that are executed outside of any explicit transaction do not drop off the radar:
|
||||
|
||||
```swift
|
||||
try dbQueue.writeWithoutTransaction { db in
|
||||
try db.execute(sql: "INSERT ...") // 1. didChange, 2. willCommit, 3. didCommit
|
||||
try db.execute(sql: "UPDATE ...") // 4. didChange, 5. willCommit, 6. didCommit
|
||||
}
|
||||
```
|
||||
|
||||
Changes that are on hold because of a [savepoint](https://www.sqlite.org/lang_savepoint.html) are only notified after the savepoint has been released. This makes sure that notified events are only those that have an opportunity to be committed:
|
||||
|
||||
```swift
|
||||
try dbQueue.inTransaction { db in
|
||||
try db.execute(sql: "INSERT ...") // 1. didChange
|
||||
|
||||
try db.execute(sql: "SAVEPOINT foo")
|
||||
try db.execute(sql: "UPDATE ...") // delayed
|
||||
try db.execute(sql: "UPDATE ...") // delayed
|
||||
try db.execute(sql: "RELEASE SAVEPOINT foo") // 2. didChange, 3. didChange
|
||||
|
||||
try db.execute(sql: "SAVEPOINT bar")
|
||||
try db.execute(sql: "UPDATE ...") // not notified
|
||||
try db.execute(sql: "ROLLBACK TO SAVEPOINT bar")
|
||||
try db.execute(sql: "RELEASE SAVEPOINT bar")
|
||||
|
||||
return .commit // 4. willCommit, 5. didCommit
|
||||
}
|
||||
```
|
||||
|
||||
Eventual errors thrown from `databaseWillCommit` are exposed to the application code:
|
||||
|
||||
```swift
|
||||
do {
|
||||
try dbQueue.inTransaction { db in
|
||||
...
|
||||
return .commit // 1. willCommit (throws), 2. didRollback
|
||||
}
|
||||
} catch {
|
||||
// 3. The error thrown by the transaction observer.
|
||||
}
|
||||
```
|
||||
|
||||
- Note: All callbacks are called in the writer dispatch queue, and serialized with all database updates.
|
||||
|
||||
- Note: The `databaseDidChange` and `databaseWillCommit` callbacks must not access the observed writer database connection in any way. This limitation does not apply to `databaseDidCommit` and `databaseDidRollback` which can use their database argument.
|
||||
|
||||
## Filtering Database Events
|
||||
|
||||
**Transaction observers can choose the database changes they are interested in.**
|
||||
|
||||
The ``observes(eventsOfKind:)`` method filters events that are notified to ``databaseDidChange(with:)``. It is the most efficient and recommended change filtering technique, because it is only called once before a database query is executed, and can completely disable change tracking:
|
||||
|
||||
```swift
|
||||
// Calls `observes(eventsOfKind:)` once.
|
||||
// Calls `databaseDidChange(with:)` for every updated row, or not at all.
|
||||
try db.execute(sql: "UPDATE player SET score = score + 1")
|
||||
```
|
||||
|
||||
The ``DatabaseEventKind`` argument of `observes(eventsOfKind:)` can distinguish insertions from deletions and updates, and is also able to tell the columns that are about to be changed.
|
||||
|
||||
For example, an observer can focus on the changes that happen on the "player" database table only:
|
||||
|
||||
```swift
|
||||
class PlayerObserver: TransactionObserver {
|
||||
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
|
||||
// Only observe changes to the "player" table.
|
||||
eventKind.tableName == "player"
|
||||
}
|
||||
|
||||
func databaseDidChange(with event: DatabaseEvent) {
|
||||
// This method is only called for changes that happen to
|
||||
// the "player" table.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of transactions.
|
||||
|
||||
## Observation Extent
|
||||
|
||||
**You can specify how long an observer is notified of database changes and transactions.**
|
||||
|
||||
The `remove(transactionObserver:)` method explicitly stops notifications, at any time:
|
||||
|
||||
```swift
|
||||
// From a database queue or pool:
|
||||
dbQueue.remove(transactionObserver: observer)
|
||||
|
||||
// From a database connection:
|
||||
dbQueue.inDatabase { db in
|
||||
db.remove(transactionObserver: observer)
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, use the `extent` parameter of the `add(transactionObserver:extent:)` method:
|
||||
|
||||
```swift
|
||||
let observer = MyObserver()
|
||||
|
||||
// On a database queue or pool:
|
||||
dbQueue.add(transactionObserver: observer) // default extent
|
||||
dbQueue.add(transactionObserver: observer, extent: .observerLifetime)
|
||||
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
|
||||
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
|
||||
|
||||
// On a database connection:
|
||||
dbQueue.inDatabase { db in
|
||||
db.add(transactionObserver: ...)
|
||||
}
|
||||
```
|
||||
|
||||
- The default extent is `.observerLifetime`: the database holds a weak reference to the observer, and the observation automatically ends when the observer is deallocated. Meanwhile, the observer is notified of all changes and transactions.
|
||||
|
||||
- `.nextTransaction` activates the observer until the current or next transaction completes. The database keeps a strong reference to the observer until its `databaseDidCommit` or `databaseDidRollback` callback is called. Hereafter the observer won't get any further notification.
|
||||
|
||||
- `.databaseLifetime` has the database retain and notify the observer until the database connection is closed.
|
||||
|
||||
Finally, an observer can avoid processing database changes until the end of the current transaction. After ``stopObservingDatabaseChangesUntilNextTransaction()``, the `databaseDidChange` callback will not be called until the current transaction completes:
|
||||
|
||||
```swift
|
||||
class PlayerObserver: TransactionObserver {
|
||||
var playerTableWasModified = false
|
||||
|
||||
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
|
||||
eventKind.tableName == "player"
|
||||
}
|
||||
|
||||
func databaseDidChange(with event: DatabaseEvent) {
|
||||
playerTableWasModified = true
|
||||
|
||||
// It is pointless to keep on tracking further changes:
|
||||
stopObservingDatabaseChangesUntilNextTransaction()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Support for SQLite Pre-Update Hooks
|
||||
|
||||
When SQLite is built with the `SQLITE_ENABLE_PREUPDATE_HOOK` option, `TransactionObserver` gets an extra callback which lets you observe individual column values in the rows modified by a transaction:
|
||||
|
||||
```swift
|
||||
protocol TransactionObserver: AnyObject {
|
||||
#if SQLITE_ENABLE_PREUPDATE_HOOK
|
||||
/// Notifies before a database change (insert, update, or delete)
|
||||
/// with change information (initial / final values for the row's
|
||||
/// columns).
|
||||
///
|
||||
/// The event is only valid for the duration of this method call. If you
|
||||
/// need to keep it longer, store a copy: event.copy().
|
||||
func databaseWillChange(with event: DatabasePreUpdateEvent)
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
This extra API can be activated in two ways:
|
||||
|
||||
1. Use the GRDB.swift CocoaPod with a custom compilation option, as below.
|
||||
|
||||
It uses the system SQLite, which is compiled with `SQLITE_ENABLE_PREUPDATE_HOOK` support, but only on iOS 11.0+ (we don't know the minimum version of macOS, tvOS, watchOS):
|
||||
|
||||
```ruby
|
||||
pod 'GRDB.swift'
|
||||
platform :ios, '11.0' # or above
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
# Enable extra GRDB APIs
|
||||
config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK"
|
||||
# Enable extra SQLite APIs
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Warning**: make sure you use the right platform version! You will get runtime errors on devices with a lower version.
|
||||
|
||||
**Note**: the `GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1` option in `GCC_PREPROCESSOR_DEFINITIONS` defines some C function prototypes that are lacking from the system `<sqlite3.h>` header. When Xcode eventually ships with an SDK that includes a complete header, you may get a compiler error about duplicate function definitions. When this happens, just remove this `GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1` option.
|
||||
|
||||
2. Use a [custom SQLite build](http://github.com/groue/GRDB.swift/blob/master/Documentation/CustomSQLiteBuilds.md) and activate the `SQLITE_ENABLE_PREUPDATE_HOOK` compilation option.
|
||||
|
||||
## Dealing with Undetected Changes
|
||||
|
||||
The changes and transactions that are not automatically notified to transaction observers are:
|
||||
|
||||
- Read-only transactions.
|
||||
- Changes and transactions performed by external database connections.
|
||||
- Changes performed by SQLite statements that are not both compiled and executed through GRDB APIs.
|
||||
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
|
||||
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
|
||||
- The deletion of duplicate rows triggered by [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) clauses (this last exception might change in a future release of SQLite).
|
||||
|
||||
To notify undetected changes to transaction observers, perform an explicit call to the ``Database/notifyChanges(in:)`` `Database` method. The ``databaseDidChange()-7olv7`` callback will be called accordingly. For example:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Notify observers that some changes were performed in the database
|
||||
try db.notifyChanges(in: .fullDatabase)
|
||||
|
||||
// Notify observers that some changes were performed in the player table
|
||||
try db.notifyChanges(in: Player.all())
|
||||
|
||||
// Equivalent alternative
|
||||
try db.notifyChanges(in: Table("player"))
|
||||
}
|
||||
```
|
||||
|
||||
To notify a change in the database schema, notify a change to the `sqlite_master` table:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Notify all observers of the sqlite_master table
|
||||
try db.notifyChanges(in: Table("sqlite_master"))
|
||||
}
|
||||
```
|
||||
|
||||
## Topics
|
||||
|
||||
### Filtering Database Changes
|
||||
|
||||
- ``observes(eventsOfKind:)``
|
||||
- ``DatabaseEventKind``
|
||||
|
||||
### Handling Database Changes
|
||||
|
||||
- ``databaseDidChange()-7olv7``
|
||||
- ``databaseDidChange(with:)``
|
||||
- ``stopObservingDatabaseChangesUntilNextTransaction()``
|
||||
- ``DatabaseEvent``
|
||||
|
||||
### Handling Transactions
|
||||
|
||||
- ``databaseWillCommit()-7mksu``
|
||||
- ``databaseDidCommit(_:)``
|
||||
- ``databaseDidRollback(_:)``
|
||||
@@ -0,0 +1,318 @@
|
||||
# ``GRDB/ValueObservation``
|
||||
|
||||
`ValueObservation` tracks changes in the results of database requests, and notifies fresh values whenever the database changes.
|
||||
|
||||
## Overview
|
||||
|
||||
`ValueObservation` tracks insertions, updates, and deletions that impact the tracked value, whether performed with raw SQL, or <doc:QueryInterface>. This includes indirect changes triggered by [foreign keys actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
|
||||
|
||||
See <doc:GRDB/ValueObservation#Dealing-with-Undetected-Changes> below for the list of exceptions.
|
||||
|
||||
## ValueObservation Usage
|
||||
|
||||
1. Make sure that a unique database connection, ``DatabaseQueue`` or ``DatabasePool``, is kept open during the whole duration of the observation.
|
||||
|
||||
2. Create a `ValueObservation` with a closure that fetches the observed value:
|
||||
|
||||
```swift
|
||||
let observation = ValueObservation.tracking { db in
|
||||
// Fetch and return the observed value
|
||||
}
|
||||
|
||||
// For example, an observation of [Player], which tracks all players:
|
||||
let observation = ValueObservation.tracking { db in
|
||||
try Player.fetchAll(db)
|
||||
}
|
||||
|
||||
// The same observation, using shorthand notation:
|
||||
let observation = ValueObservation.tracking(Player.fetchAll)
|
||||
```
|
||||
|
||||
There is no limit on the values that can be observed. An observation can perform multiple requests, from multiple database tables, and use raw SQL. See ``tracking(_:)`` for some examples.
|
||||
|
||||
3. Start the observation in order to be notified of changes:
|
||||
|
||||
```swift
|
||||
let cancellable = observation.start(in: dbQueue) { error in
|
||||
// Handle error
|
||||
} onChange: { (players: [Player]) in
|
||||
print("Fresh players", players)
|
||||
}
|
||||
```
|
||||
|
||||
4. Stop the observation by calling the ``DatabaseCancellable/cancel()`` method on the object returned by the `start` method. Cancellation is automatic when the cancellable is deallocated:
|
||||
|
||||
```swift
|
||||
cancellable.cancel()
|
||||
```
|
||||
|
||||
`ValueObservation` can also be turned into an async sequence, a Combine publisher, or an RxSwift observable (see the companion library [RxGRDB](https://github.com/RxSwiftCommunity/RxGRDB)):
|
||||
|
||||
- Async sequence:
|
||||
|
||||
```swift
|
||||
do {
|
||||
for try await players in observation.values(in: dbQueue) {
|
||||
print("Fresh players", players)
|
||||
}
|
||||
} catch {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
- Combine Publisher:
|
||||
|
||||
```swift
|
||||
let cancellable = observation.publisher(in: dbQueue).sink { completion in
|
||||
// Handle completion
|
||||
} receiveValue: { (players: [Player]) in
|
||||
print("Fresh players", players)
|
||||
}
|
||||
```
|
||||
|
||||
## ValueObservation Behavior
|
||||
|
||||
`ValueObservation` notifies an initial value before the eventual changes.
|
||||
|
||||
`ValueObservation` only notifies changes committed to disk.
|
||||
|
||||
By default, `ValueObservation` notifies a fresh value whenever any component of its fetched value is modified (any fetched column, row, etc.). This can be configured: see <doc:ValueObservation#Specifying-the-Tracked-Region>.
|
||||
|
||||
By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously. This can be configured: see <doc:ValueObservation#ValueObservation-Scheduling>.
|
||||
|
||||
By default, `ValueObservation` fetches a fresh value immediately after a change is committed in the database. In particular, modifying the database on the main thread triggers a fetch on the main thread as well. This behavior can be configured: see <doc:ValueObservation#ValueObservation-Scheduling>.
|
||||
|
||||
`ValueObservation` may coalesce subsequent changes into a single notification.
|
||||
|
||||
`ValueObservation` may notify consecutive identical values. You can filter out the undesired duplicates with the ``removeDuplicates()`` method.
|
||||
|
||||
Starting an observation retains the database connection, until it is stopped. As long as the observation is active, the database connection won't be deallocated.
|
||||
|
||||
The database observation stops when the cancellable returned by the `start` method is cancelled or deallocated, or if an error occurs.
|
||||
|
||||
> Important: Take care that there are use cases that `ValueObservation` is unfit for.
|
||||
>
|
||||
> For example, an application may need to process absolutely all changes, and avoid any coalescing. An application may also need to process changes before any further modifications could be performed in the database file. In those cases, the application needs to track *individual transactions*, not values: use ``DatabaseRegionObservation``.
|
||||
>
|
||||
> If you need to process changes before they are committed to disk, use ``TransactionObserver``.
|
||||
|
||||
## ValueObservation Scheduling
|
||||
|
||||
By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously:
|
||||
|
||||
```swift
|
||||
// The default scheduling
|
||||
let cancellable = observation.start(in: dbQueue) { error in
|
||||
// Called asynchronously on the main dispatch queue
|
||||
} onChange: { value in
|
||||
// Called asynchronously on the main dispatch queue
|
||||
print("Fresh value", value)
|
||||
}
|
||||
```
|
||||
|
||||
You can change this behavior by adding a `scheduling` argument to the `start()` method.
|
||||
|
||||
For example, the ``ValueObservationScheduler/immediate`` scheduler notifies all values on the main dispatch queue, and notifies the first one immediately when the observation starts.
|
||||
|
||||
It is very useful in graphic applications, because you can configure views right away, without waiting for the initial value to be fetched eventually. You don't have to implement any empty or loading screen, or to prevent some undesired initial animation. Take care that the user interface is not responsive during the fetch of the first value, so only use the `immediate` scheduling for very fast database requests!
|
||||
|
||||
The `immediate` scheduling requires that the observation starts from the main dispatch queue (a fatal error is raised otherwise):
|
||||
|
||||
```swift
|
||||
// Immediate scheduling notifies
|
||||
// the initial value right on subscription.
|
||||
let cancellable = observation
|
||||
.start(in: dbQueue, scheduling: .immediate) { error in
|
||||
// Called on the main dispatch queue
|
||||
} onChange: { value in
|
||||
// Called on the main dispatch queue
|
||||
print("Fresh value", value)
|
||||
}
|
||||
// <- Here "Fresh value" has already been printed.
|
||||
```
|
||||
|
||||
The other built-in scheduler ``ValueObservationScheduler/async(onQueue:)`` asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications:
|
||||
|
||||
```swift
|
||||
// Async scheduling notifies all values
|
||||
// on the specified dispatch queue.
|
||||
let myQueue: DispatchQueue
|
||||
let cancellable = observation
|
||||
.start(in: dbQueue, scheduling: .async(myQueue)) { error in
|
||||
// Called asynchronously on myQueue
|
||||
} onChange: { value in
|
||||
// Called asynchronously on myQueue
|
||||
print("Fresh value", value)
|
||||
}
|
||||
```
|
||||
|
||||
As described above, the `scheduling` argument controls the execution of the change and error callbacks. You also have some control on the execution of the database fetch:
|
||||
|
||||
- With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main thread, when the observation starts, so that the initial value can be notified immediately.
|
||||
|
||||
- With the default `.async` scheduling, the initial fetch is always performed asynchronouly. It never blocks the main thread.
|
||||
|
||||
- By default, fresh values are fetched immediately after the database was changed. In particular, modifying the database on the main thread triggers a fetch on the main thread as well.
|
||||
|
||||
To change this behavior, and guarantee that fresh values are never fetched from the main thread, you need a ``DatabasePool`` and an optimized observation created with the ``tracking(regions:fetch:)`` or ``trackingConstantRegion(_:)`` methods. Make sure you read the documentation of those methods, or you might write an observation that misses some database changes.
|
||||
|
||||
It is possible to use a ``DatabasePool`` in the application, and an in-memory ``DatabaseQueue`` in tests and Xcode previews, with the common protocol ``DatabaseWriter``.
|
||||
|
||||
|
||||
## ValueObservation Sharing
|
||||
|
||||
Sharing a `ValueObservation` spares database resources. When a database change happens, a fresh value is fetched only once, and then notified to all clients of the shared observation.
|
||||
|
||||
You build a shared observation with ``shared(in:scheduling:extent:)``:
|
||||
|
||||
```swift
|
||||
// SharedValueObservation<[Player]>
|
||||
let sharedObservation = ValueObservation
|
||||
.tracking { db in try Player.fetchAll(db) }
|
||||
.shared(in: dbQueue)
|
||||
```
|
||||
|
||||
`ValueObservation` and `SharedValueObservation` are nearly identical, but the latter has no operator such as `map`. As a replacement, you may for example use Combine apis:
|
||||
|
||||
```swift
|
||||
let cancellable = try sharedObservation
|
||||
.publisher() // Turn shared observation into a Combine Publisher
|
||||
.map { ... } // The map operator from Combine
|
||||
.sink(...)
|
||||
```
|
||||
|
||||
|
||||
## Specifying the Tracked Region
|
||||
|
||||
While the standard ``tracking(_:)`` method lets you track changes to a fetched value and receive any changes to it, sometimes your use case might require more granular control.
|
||||
|
||||
Consider a scenario where you'd like to get a specific Player's row, but only when their `score` column changes. You can use ``tracking(region:_:fetch:)`` to do just that:
|
||||
|
||||
```swift
|
||||
let observation = ValueObservation.tracking(
|
||||
// Define the tracked database region
|
||||
// (the score column of the player with id 1)
|
||||
region: Player.select(Column("score")).filter(id: 1),
|
||||
// Define what to fetch upon such change to the tracked region
|
||||
// (the player with id 1)
|
||||
fetch: { db in try Player.fetchOne(db, id: 1) }
|
||||
)
|
||||
```
|
||||
|
||||
This ``tracking(region:_:fetch:)`` method lets you entirely separate the **observed region(s)** from the **fetched value** itself, for maximum flexibility. See ``DatabaseRegionConvertible`` for more information about the regions that can be tracked.
|
||||
|
||||
## Dealing with Undetected Changes
|
||||
|
||||
`ValueObservation` will not fetch and notify a fresh value whenever the database is modified in an undetectable way:
|
||||
|
||||
- Changes performed by external database connections.
|
||||
- Changes performed by SQLite statements that are not compiled and executed by GRDB.
|
||||
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
|
||||
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
|
||||
|
||||
To have observations notify a fresh values after such an undetected change was performed, applications can take explicit action. For example, cancel and restart observations. Alternatively, call the ``Database/notifyChanges(in:)`` `Database` method from a write transaction:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Notify observations that some changes were performed in the database
|
||||
try db.notifyChanges(in: .fullDatabase)
|
||||
|
||||
// Notify observations that some changes were performed in the player table
|
||||
try db.notifyChanges(in: Player.all())
|
||||
|
||||
// Equivalent alternative
|
||||
try db.notifyChanges(in: Table("player"))
|
||||
}
|
||||
```
|
||||
|
||||
## ValueObservation Performance
|
||||
|
||||
This section further describes runtime aspects of `ValueObservation`, and provides some optimization tips for demanding applications.
|
||||
|
||||
**`ValueObservation` is triggered by database transactions that may modify the tracked value.**
|
||||
|
||||
Precisely speaking, `ValueObservation` tracks changes in a ``DatabaseRegion``, not changes in values.
|
||||
|
||||
For example, if you track the maximum score of players, all transactions that impact the `score` column of the `player` database table (any update, insertion, or deletion) trigger the observation, even if the maximum score itself is not changed.
|
||||
|
||||
You can filter out undesired duplicate notifications with the ``removeDuplicates()`` method.
|
||||
|
||||
**ValueObservation can create database contention.** In other words, active observations take a toll on the constrained database resources. When triggered by impactful transactions, observations fetch fresh values, and can delay read and write database accesses of other application components.
|
||||
|
||||
When needed, you can help GRDB optimize observations and reduce database contention:
|
||||
|
||||
> Tip: Stop observations when possible.
|
||||
>
|
||||
> For example, if a `UIViewController` needs to display database values, it can start the observation in `viewWillAppear`, and stop it in `viewWillDisappear`.
|
||||
>
|
||||
> In a SwiftUI application, you can profit from the [GRDBQuery](https://github.com/groue/GRDBQuery) companion library, and its [`View.queryObservation(_:)`](https://swiftpackageindex.com/groue/grdbquery/documentation/grdbquery/queryobservation) method.
|
||||
|
||||
> Tip: Share observations when possible.
|
||||
>
|
||||
> Each call to `ValueObservation.start` method triggers independent values refreshes. When several components of your app are interested in the same value, consider sharing the observation with ``shared(in:scheduling:extent:)``.
|
||||
|
||||
> Tip: When the observation processes some raw fetched values, use the ``map(_:)`` operator:
|
||||
>
|
||||
> ```swift
|
||||
> // Plain observation
|
||||
> let observation = ValueObservation.tracking { db -> MyValue in
|
||||
> let players = try Player.fetchAll(db)
|
||||
> return computeMyValue(players)
|
||||
> }
|
||||
>
|
||||
> // Optimized observation
|
||||
> let observation = ValueObservation
|
||||
> .tracking { db try Player.fetchAll(db) }
|
||||
> .map { players in computeMyValue(players) }
|
||||
> ```
|
||||
>
|
||||
> The `map` operator performs its job without blocking database accesses, and without blocking the main thread.
|
||||
|
||||
> Tip: When the observation tracks a constant database region, create an optimized observation with the ``tracking(regions:fetch:)`` or ``trackingConstantRegion(_:)`` methods. Make sure you read the documentation of those methods, or you might write an observation that misses some database changes.
|
||||
|
||||
**Truncating WAL checkpoints impact ValueObservation.** Such checkpoints are performed with ``Database/checkpoint(_:on:)`` or [`PRAGMA wal_checkpoint`](https://www.sqlite.org/pragma.html#pragma_wal_checkpoint). When an observation is started on a ``DatabasePool``, from a database that has a missing or empty [wal file](https://www.sqlite.org/tempfiles.html#write_ahead_log_wal_files), the observation will always notify two values when it starts, even if the database content is not changed. This is a consequence of the impossibility to create the [wal snapshot](https://www.sqlite.org/c3ref/snapshot_get.html) needed for detecting that no changes were performed during the observation startup. If your application performs truncating checkpoints, you will avoid this behavior if you recreate a non-empty wal file before starting observations. To do so, perform any kind of no-op transaction (such a creating and dropping a dummy table).
|
||||
|
||||
|
||||
## Topics
|
||||
|
||||
### Creating a ValueObservation
|
||||
|
||||
- ``tracking(_:)``
|
||||
- ``trackingConstantRegion(_:)``
|
||||
- ``tracking(region:_:fetch:)``
|
||||
- ``tracking(regions:fetch:)``
|
||||
|
||||
### Creating a Shared Observation
|
||||
|
||||
- ``shared(in:scheduling:extent:)``
|
||||
- ``SharedValueObservationExtent``
|
||||
|
||||
### Accessing Observed Values
|
||||
|
||||
- ``publisher(in:scheduling:)``
|
||||
- ``start(in:scheduling:onError:onChange:)``
|
||||
- ``values(in:scheduling:bufferingPolicy:)``
|
||||
- ``DatabaseCancellable``
|
||||
- ``ValueObservationScheduler``
|
||||
|
||||
### Mapping Values
|
||||
|
||||
- ``map(_:)``
|
||||
|
||||
### Filtering Values
|
||||
|
||||
- ``removeDuplicates()``
|
||||
- ``removeDuplicates(by:)``
|
||||
|
||||
### Requiring Write Access
|
||||
|
||||
- ``requiresWriteAccess``
|
||||
|
||||
### Debugging
|
||||
|
||||
- ``handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)``
|
||||
- ``print(_:to:)``
|
||||
|
||||
### Support
|
||||
|
||||
- ``ValueReducer``
|
||||
@@ -0,0 +1,15 @@
|
||||
# Full-Text Search
|
||||
|
||||
Search a corpus of textual documents.
|
||||
|
||||
## Overview
|
||||
|
||||
Please refer to the [Full-Text Search](https://github.com/groue/GRDB.swift/blob/master/Documentation/FullTextSearch.md) guide. It also describes how to enable support for the FTS5 engine.
|
||||
|
||||
## Topics
|
||||
|
||||
### Full-Text Engines
|
||||
|
||||
- ``FTS3``
|
||||
- ``FTS4``
|
||||
- ``FTS5``
|
||||
@@ -0,0 +1,103 @@
|
||||
# ``GRDB``
|
||||
|
||||
A toolkit for SQLite databases, with a focus on application development
|
||||
|
||||
##
|
||||
|
||||

|
||||
|
||||
## Overview
|
||||
|
||||
Use this library to save your application’s permanent data into SQLite databases. It comes with built-in tools that address common needs:
|
||||
|
||||
- **SQL Generation**
|
||||
|
||||
Enhance your application models with persistence and fetching methods, so that you don't have to deal with SQL and raw database rows when you don't want to.
|
||||
|
||||
- **Database Observation**
|
||||
|
||||
Get notifications when database values are modified.
|
||||
|
||||
- **Robust Concurrency**
|
||||
|
||||
Multi-threaded applications can efficiently use their databases, including WAL databases that support concurrent reads and writes.
|
||||
|
||||
- **Migrations**
|
||||
|
||||
Evolve the schema of your database as you ship new versions of your application.
|
||||
|
||||
- **Leverage your SQLite skills**
|
||||
|
||||
Not all developers need advanced SQLite features. But when you do, GRDB is as sharp as you want it to be. Come with your SQL and SQLite skills, or learn new ones as you go!
|
||||
|
||||
## Usage
|
||||
|
||||
Start using the database in four steps:
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
|
||||
// 1. Open a database connection
|
||||
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
|
||||
|
||||
// 2. Define the database schema
|
||||
try dbQueue.write { db in
|
||||
try db.create(table: "player") { t in
|
||||
t.primaryKey("id", .text)
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Define a record type
|
||||
struct Player: Codable, FetchableRecord, PersistableRecord {
|
||||
var id: String
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
|
||||
// 4. Write and read in the database
|
||||
try dbQueue.write { db in
|
||||
try Player(id: "1", name: "Arthur", score: 100).insert(db)
|
||||
try Player(id: "2", name: "Barbara", score: 1000).insert(db)
|
||||
}
|
||||
|
||||
let players: [Player] = try dbQueue.read { db in
|
||||
try Player.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
## Links and Companion Libraries
|
||||
|
||||
- [GitHub Repository](http://github.com/groue/GRDB.swift)
|
||||
- [Installation Instructions, encryption with SQLCipher, custom SQLite builds](https://github.com/groue/GRDB.swift#installation)
|
||||
- [GRDBQuery](https://github.com/groue/GRDBQuery): the SwiftUI companion for GRDB.
|
||||
- [GRDBSnapshotTesting](https://github.com/groue/GRDBSnapshotTesting): Test your database.
|
||||
|
||||
## Topics
|
||||
|
||||
### Fundamentals
|
||||
|
||||
- <doc:DatabaseConnections>
|
||||
- <doc:SQLSupport>
|
||||
- <doc:Concurrency>
|
||||
- <doc:Transactions>
|
||||
|
||||
### Migrations and The Database Schema
|
||||
|
||||
- <doc:DatabaseSchema>
|
||||
- <doc:Migrations>
|
||||
|
||||
### Records and the Query Interface
|
||||
|
||||
- <doc:QueryInterface>
|
||||
- <doc:RecordRecommendedPractices>
|
||||
- <doc:RecordTimestamps>
|
||||
- <doc:SingleRowTables>
|
||||
|
||||
### Application Tools
|
||||
|
||||
- <doc:DatabaseObservation>
|
||||
- <doc:FullTextSearch>
|
||||
- <doc:JSON>
|
||||
- ``DatabasePublishers``
|
||||
@@ -0,0 +1,150 @@
|
||||
# JSON Support
|
||||
|
||||
Store and use JSON values in SQLite databases.
|
||||
|
||||
## Overview
|
||||
|
||||
SQLite and GRDB can store and fetch JSON values in database columns. Starting iOS 16+, macOS 10.15+, tvOS 17+, and watchOS 9+, JSON values can be manipulated at the database level.
|
||||
|
||||
## Store and fetch JSON values
|
||||
|
||||
### JSON columns in the database schema
|
||||
|
||||
It is recommended to store JSON values in text columns. In the example below, we create a ``Database/ColumnType/jsonText`` column with ``Database/create(table:options:body:)``:
|
||||
|
||||
```swift
|
||||
try db.create(table: "player") { t in
|
||||
t.primaryKey("id", .text)
|
||||
t.column("name", .text).notNull()
|
||||
t.column("address", .jsonText).notNull() // A JSON column
|
||||
}
|
||||
```
|
||||
|
||||
> Note: `.jsonText` and `.text` are equivalent, because both build a TEXT column in SQL. Yet the former better describes the intent of the column.
|
||||
>
|
||||
> Note: SQLite JSON functions and operators are [documented](https://www.sqlite.org/json1.html#interface_overview) to throw errors if any of their arguments are binary blobs. That's the reason why it is recommended to store JSON as text.
|
||||
|
||||
> Tip: When an application performs queries on values embedded inside JSON columns, indexes can help performance:
|
||||
>
|
||||
> ```swift
|
||||
> // CREATE INDEX "player_on_country"
|
||||
> // ON "player"("address" ->> 'country')
|
||||
> try db.create(
|
||||
> index: "player_on_country",
|
||||
> on: "player",
|
||||
> expressions: [
|
||||
> JSONColumn("address")["country"],
|
||||
> ])
|
||||
>
|
||||
> // SELECT * FROM player
|
||||
> // WHERE "address" ->> 'country' = 'DE'
|
||||
> let germanPlayers = try Player
|
||||
> .filter(JSONColumn("address")["country"] == "DE")
|
||||
> .fetchAll(db)
|
||||
> ```
|
||||
|
||||
### Strict and flexible JSON schemas
|
||||
|
||||
[Codable Records](https://github.com/groue/GRDB.swift/blob/master/README.md#codable-records) handle both strict and flexible JSON schemas.
|
||||
|
||||
**For strict schemas**, use `Codable` properties. They will be stored as JSON strings in the database:
|
||||
|
||||
```swift
|
||||
struct Address: Codable {
|
||||
var street: String
|
||||
var city: String
|
||||
var country: String
|
||||
}
|
||||
|
||||
struct Player: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
|
||||
// Stored as a JSON string
|
||||
// {"street": "...", "city": "...", "country": "..."}
|
||||
var address: Address
|
||||
}
|
||||
|
||||
extension Player: FetchableRecord, PersistableRecord { }
|
||||
```
|
||||
|
||||
**For flexible schemas**, use `String` or `Data` properties.
|
||||
|
||||
In the specific case of `Data` properties, it is recommended to store them as text in the database, because SQLite JSON functions and operators are [documented](https://www.sqlite.org/json1.html#interface_overview) to throw errors if any of their arguments are binary blobs. This encoding is automatic with ``DatabaseDataEncodingStrategy/text``:
|
||||
|
||||
```swift
|
||||
// JSON String property
|
||||
struct Player: Codable {
|
||||
var id: String
|
||||
var name: String
|
||||
var address: String // JSON string
|
||||
}
|
||||
|
||||
extension Player: FetchableRecord, PersistableRecord { }
|
||||
|
||||
// JSON Data property, saved as text in the database
|
||||
struct Team: Codable {
|
||||
var id: String
|
||||
var color: String
|
||||
var info: Data // JSON UTF8 data
|
||||
}
|
||||
|
||||
extension Team: FetchableRecord, PersistableRecord {
|
||||
// Support SQLite JSON functions and operators
|
||||
// by storing JSON data as database text:
|
||||
static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
|
||||
}
|
||||
```
|
||||
|
||||
## Manipulate JSON values at the database level
|
||||
|
||||
[SQLite JSON functions and operators](https://www.sqlite.org/json1.html) are available starting iOS 16+, macOS 10.15+, tvOS 17+, and watchOS 9+.
|
||||
|
||||
Functions such as `JSON`, `JSON_EXTRACT`, `JSON_PATCH` and others are available as static methods on `Database`: ``Database/json(_:)``, ``Database/jsonExtract(_:atPath:)``, ``Database/jsonPatch(_:with:)``, etc.
|
||||
|
||||
See the full list below.
|
||||
|
||||
## JSON table-valued functions
|
||||
|
||||
The JSON table-valued functions `json_each` and `json_tree` are not supported.
|
||||
|
||||
## Topics
|
||||
|
||||
### JSON Values
|
||||
|
||||
- ``SQLJSONExpressible``
|
||||
- ``JSONColumn``
|
||||
|
||||
### Access JSON subcomponents, and query JSON values, at the SQL level
|
||||
|
||||
The `->` and `->>` SQL operators are available on the ``SQLJSONExpressible`` protocol.
|
||||
|
||||
- ``Database/jsonArrayLength(_:)``
|
||||
- ``Database/jsonArrayLength(_:atPath:)``
|
||||
- ``Database/jsonExtract(_:atPath:)``
|
||||
- ``Database/jsonExtract(_:atPaths:)``
|
||||
- ``Database/jsonType(_:)``
|
||||
- ``Database/jsonType(_:atPath:)``
|
||||
|
||||
### Build new JSON values at the SQL level
|
||||
|
||||
- ``Database/json(_:)``
|
||||
- ``Database/jsonArray(_:)-8xxe3``
|
||||
- ``Database/jsonArray(_:)-469db``
|
||||
- ``Database/jsonObject(_:)``
|
||||
- ``Database/jsonQuote(_:)``
|
||||
- ``Database/jsonGroupArray(_:filter:)``
|
||||
- ``Database/jsonGroupObject(key:value:filter:)``
|
||||
|
||||
### Modify JSON values at the SQL level
|
||||
|
||||
- ``Database/jsonInsert(_:_:)``
|
||||
- ``Database/jsonPatch(_:with:)``
|
||||
- ``Database/jsonReplace(_:_:)``
|
||||
- ``Database/jsonRemove(_:atPath:)``
|
||||
- ``Database/jsonRemove(_:atPaths:)``
|
||||
- ``Database/jsonSet(_:_:)``
|
||||
|
||||
### Validate JSON values at the SQL level
|
||||
|
||||
- ``Database/jsonIsValid(_:)``
|
||||
@@ -0,0 +1,250 @@
|
||||
# Migrations
|
||||
|
||||
Migrations allow you to evolve your database schema over time.
|
||||
|
||||
## Overview
|
||||
|
||||
You can think of migrations as being 'versions' of the database. A database schema starts off in an empty state, and each migration adds or removes tables, columns, or entries.
|
||||
|
||||
GRDB can update the database schema along this timeline, bringing it from whatever point it is in the history to the latest version. When a user upgrades your application, only non-applied migrations are run.
|
||||
|
||||
You setup migrations in a ``DatabaseMigrator`` instance. For example:
|
||||
|
||||
```swift
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
// 1st migration
|
||||
migrator.registerMigration("Create authors") { db in
|
||||
try db.create(table: "author") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("creationDate", .datetime)
|
||||
t.column("name", .text)
|
||||
}
|
||||
}
|
||||
|
||||
// 2nd migration
|
||||
migrator.registerMigration("Add books and author.birthYear") { db in
|
||||
try db.create(table: "book") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.belongsTo("author").notNull()
|
||||
t.column("title", .text).notNull()
|
||||
}
|
||||
|
||||
try db.alter(table: "author") { t in
|
||||
t.add(column: "birthYear", .integer)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To migrate a database, open a connection (see <doc:DatabaseConnections>), and call the ``DatabaseMigrator/migrate(_:)`` method:
|
||||
|
||||
```swift
|
||||
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
|
||||
|
||||
// Migrate the database up to the latest version
|
||||
try migrator.migrate(dbQueue)
|
||||
```
|
||||
|
||||
You can also migrate a database up to a specific version (useful for testing):
|
||||
|
||||
```swift
|
||||
try migrator.migrate(dbQueue, upTo: "v2")
|
||||
|
||||
// Migrations can only run forward:
|
||||
try migrator.migrate(dbQueue, upTo: "v2")
|
||||
try migrator.migrate(dbQueue, upTo: "v1")
|
||||
// ^ fatal error: database is already migrated beyond migration "v1"
|
||||
```
|
||||
|
||||
When several versions of your app are deployed in the wild, you may want to perform extra checks:
|
||||
|
||||
```swift
|
||||
try dbQueue.read { db in
|
||||
// Read-only apps or extensions may want to check if the database
|
||||
// lacks expected migrations:
|
||||
if try migrator.hasCompletedMigrations(db) == false {
|
||||
// database too old
|
||||
}
|
||||
|
||||
// Some apps may want to check if the database
|
||||
// contains unknown (future) migrations:
|
||||
if try migrator.hasBeenSuperseded(db) {
|
||||
// database too new
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Each migration runs in a separate transaction.** Should one throw an error, its transaction is rollbacked, subsequent migrations do not run, and the error is eventually thrown by ``DatabaseMigrator/migrate(_:)``.
|
||||
|
||||
**Migrations run with deferred foreign key checks.** This means that eventual foreign key violations are only checked at the end of the migration (and they make the migration fail). See <doc:Migrations#Foreign-Key-Checks> below for more information.
|
||||
|
||||
**The memory of applied migrations is stored in the database itself** (in a reserved table).
|
||||
|
||||
## Defining the Database Schema from a Migration
|
||||
|
||||
See <doc:DatabaseSchema> for the methods that define the database schema. For example:
|
||||
|
||||
```swift
|
||||
migrator.registerMigration("Create authors") { db in
|
||||
try db.create(table: "author") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("creationDate", .datetime)
|
||||
t.column("name", .text)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When you need to modify a table in a way that is not directly supported by SQLite, or not available on your target operating system, you will need to recreate the database table.
|
||||
|
||||
For example:
|
||||
|
||||
```swift
|
||||
migrator.registerMigration("Add NOT NULL check on author.name") { db in
|
||||
try db.create(table: "new_author") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("creationDate", .datetime)
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
try db.execute(sql: "INSERT INTO new_author SELECT * FROM author")
|
||||
try db.drop(table: "author")
|
||||
try db.rename(table: "new_author", to: "author")
|
||||
}
|
||||
```
|
||||
|
||||
The detailed sequence of operations for recreating a database table from a migration is:
|
||||
|
||||
1. When relevant, remember the format of all indexes, triggers, and views associated with table X. This information will be needed in steps 6 and 7 below. One way to do this is to run a query like the following: `SELECT type, sql FROM sqlite_schema WHERE tbl_name='X'`.
|
||||
|
||||
2. Use `CREATE TABLE` to construct a new table "new_X" that is in the desired revised format of table X. Make sure that the name "new_X" does not collide with any existing table name, of course.
|
||||
|
||||
3. Transfer content from X into new_X using a statement like: `INSERT INTO new_X SELECT ... FROM X`.
|
||||
|
||||
4. Drop the old table X: `DROP TABLE X`.
|
||||
|
||||
5. Change the name of new_X to X using: `ALTER TABLE new_X RENAME TO X`.
|
||||
|
||||
6. When relevant, use `CREATE INDEX`, `CREATE TRIGGER`, and `CREATE VIEW` to reconstruct indexes, triggers, and views associated with table X. Perhaps use the old format of the triggers, indexes, and views saved from step 3 above as a guide, making changes as appropriate for the alteration.
|
||||
|
||||
7. If any views refer to table X in a way that is affected by the schema change, then drop those views using `DROP VIEW` and recreate them with whatever changes are necessary to accommodate the schema change using `CREATE VIEW`.
|
||||
|
||||
> Important: When recreating a table, be sure to follow the above procedure exactly, in the given order, or you might corrupt triggers, views, and foreign key constraints.
|
||||
>
|
||||
> When you want to recreate a table _outside of a migration_, check the full procedure detailed in the [Making Other Kinds Of Table Schema Changes](https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes) section of the SQLite documentation.
|
||||
|
||||
## Good Practices for Defining Migrations
|
||||
|
||||
**A good migration is a migration that is never modified once it has shipped.**
|
||||
|
||||
It is much easier to control the schema of all databases deployed on users' devices when migrations define a stable timeline of schema versions. For this reason, it is recommended that migrations define the database schema with **strings**:
|
||||
|
||||
```swift
|
||||
migrator.registerMigration("Create authors") { db in
|
||||
// RECOMMENDED
|
||||
try db.create(table: "author") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
...
|
||||
}
|
||||
|
||||
// NOT RECOMMENDED
|
||||
try db.create(table: Author.databaseTableName) { t in
|
||||
t.autoIncrementedPrimaryKey(Author.Columns.id.name)
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In other words, migrations should talk to the database, only to the database, and use the database language. This makes sure the Swift code of any given migrations will never have to change in the future.
|
||||
|
||||
Migrations and the rest of the application code do not live at the same "moment". Migrations describe the past states of the database, while the rest of the application code targets the latest one only. This difference is the reason why **migrations should not depend on application types.**
|
||||
|
||||
## The eraseDatabaseOnSchemaChange Option
|
||||
|
||||
A `DatabaseMigrator` can automatically wipe out the full database content, and recreate the whole database from scratch, if it detects that migrations have changed their definition.
|
||||
|
||||
Setting ``DatabaseMigrator/eraseDatabaseOnSchemaChange`` is useful during application development, as you are still designing migrations, and the schema changes often:
|
||||
|
||||
- A migration is removed, or renamed.
|
||||
- A schema change is detected: any difference in the `sqlite_master` table, which contains the SQL used to create database tables, indexes, triggers, and views.
|
||||
|
||||
> Warning: This option can destroy your precious users' data!
|
||||
|
||||
It is recommended that this option does not ship in the released application: hide it behind `#if DEBUG` as below.
|
||||
|
||||
```swift
|
||||
var migrator = DatabaseMigrator()
|
||||
#if DEBUG
|
||||
// Speed up development by nuking the database when migrations change
|
||||
migrator.eraseDatabaseOnSchemaChange = true
|
||||
#endif
|
||||
```
|
||||
|
||||
## Foreign Key Checks
|
||||
|
||||
By default, each migration temporarily disables foreign keys, and performs a full check of all foreign keys in the database before it is committed on disk.
|
||||
|
||||
When the database becomes very big, those checks may have a noticeable impact on migration performances. You'll know this by profiling migrations, and looking for the time spent in the `checkForeignKeys` method.
|
||||
|
||||
You can make those migrations faster, but this requires a little care.
|
||||
|
||||
**Your first mitigation technique is immediate foreign key checks.**
|
||||
|
||||
When you register a migration with `.immediate` foreign key checks, the migration does not temporarily disable foreign keys, and does not need to perform a deferred full check of all foreign keys in the database:
|
||||
|
||||
```swift
|
||||
migrator.registerMigration("Fast migration", foreignKeyChecks: .immediate) { db in ... }
|
||||
```
|
||||
|
||||
Such a migration is faster, and it still guarantees database integrity. But it must only execute schema alterations directly supported by SQLite. Migrations that recreate tables as described in <doc:Migrations#Defining-the-Database-Schema-from-a-Migration> **must not** run with immediate foreign keys checks. You'll need to use the second mitigation technique:
|
||||
|
||||
**Your second mitigation technique is to disable deferred foreign key checks.**
|
||||
|
||||
You can ask the migrator to stop performing foreign key checks for all newly registered migrations:
|
||||
|
||||
```swift
|
||||
migrator = migrator.disablingDeferredForeignKeyChecks()
|
||||
```
|
||||
|
||||
Migrations become unchecked by default, and run faster. But your app becomes responsible for preventing foreign key violations from being committed to disk:
|
||||
|
||||
```swift
|
||||
migrator = migrator.disablingDeferredForeignKeyChecks()
|
||||
migrator.registerMigration("Fast but unchecked migration") { db in ... }
|
||||
```
|
||||
|
||||
To prevent a migration from committing foreign key violations on disk, you can:
|
||||
|
||||
- Register the migration with immediate foreign key checks, as long as it does not recreate tables as described in <doc:Migrations#Defining-the-Database-Schema-from-a-Migration>:
|
||||
|
||||
```swift
|
||||
migrator = migrator.disablingDeferredForeignKeyChecks()
|
||||
migrator.registerMigration("Fast and checked migration", foreignKeyChecks: .immediate) { db in ... }
|
||||
```
|
||||
|
||||
- Perform foreign key checks on some tables only, before the migration is committed on disk:
|
||||
|
||||
```swift
|
||||
migrator = migrator.disablingDeferredForeignKeyChecks()
|
||||
migrator.registerMigration("Partially checked") { db in
|
||||
...
|
||||
|
||||
// Throws an error and stops migrations if there exists a
|
||||
// foreign key violation in the 'book' table.
|
||||
try db.checkForeignKeys(in: "book")
|
||||
}
|
||||
```
|
||||
|
||||
As in the above example, check for foreign key violations with the ``Database/checkForeignKeys()`` and ``Database/checkForeignKeys(in:in:)`` methods. They throw a nicely detailed ``DatabaseError`` that contains a lot of debugging information:
|
||||
|
||||
```swift
|
||||
// SQLite error 19: FOREIGN KEY constraint violation - from book(authorId) to author(id),
|
||||
// in [id:1 authorId:2 name:"Moby-Dick"]
|
||||
try db.checkForeignKeys(in: "book")
|
||||
```
|
||||
|
||||
Alternatively, you can deal with each individual violation by iterating a cursor of ``ForeignKeyViolation``.
|
||||
|
||||
## Topics
|
||||
|
||||
### DatabaseMigrator
|
||||
|
||||
- ``DatabaseMigrator``
|
||||
@@ -0,0 +1,50 @@
|
||||
# Records and the Query Interface
|
||||
|
||||
Record types and the query interface build SQL queries for you.
|
||||
|
||||
## Overview
|
||||
|
||||
For an overview, see [Records](https://github.com/groue/GRDB.swift/blob/master/README.md#records), and [The Query Interface](https://github.com/groue/GRDB.swift/blob/master/README.md#the-query-interface).
|
||||
|
||||
## Topics
|
||||
|
||||
### Records
|
||||
|
||||
- ``Record``
|
||||
- ``EncodableRecord``
|
||||
- ``FetchableRecord``
|
||||
- ``MutablePersistableRecord``
|
||||
- ``PersistableRecord``
|
||||
- ``TableRecord``
|
||||
|
||||
### Expressions
|
||||
|
||||
- ``Column``
|
||||
- ``JSONColumn``
|
||||
- ``SQLExpression``
|
||||
|
||||
### Requests
|
||||
|
||||
- ``CommonTableExpression``
|
||||
- ``QueryInterfaceRequest``
|
||||
- ``Table``
|
||||
|
||||
### Associations
|
||||
|
||||
- ``Association``
|
||||
|
||||
### Errors
|
||||
|
||||
- ``RecordError``
|
||||
- ``PersistenceError``
|
||||
|
||||
### Supporting Types
|
||||
|
||||
- ``ColumnExpression``
|
||||
- ``DerivableRequest``
|
||||
- ``SQLExpressible``
|
||||
- ``SQLJSONExpressible``
|
||||
- ``SQLSpecificExpressible``
|
||||
- ``SQLSubqueryable``
|
||||
- ``SQLOrderingTerm``
|
||||
- ``SQLSelectable``
|
||||
@@ -0,0 +1,596 @@
|
||||
# Recommended Practices for Designing Record Types
|
||||
|
||||
Leverage the best of record types and associations.
|
||||
|
||||
## Overview
|
||||
|
||||
GRDB sits right between low-level SQLite wrappers, and high-level ORMs like [Core Data], so you may face questions when designing the model layer of your application.
|
||||
|
||||
This is the topic of this article. Examples will be illustrated with a simple library database made of books and their authors.
|
||||
|
||||
## Trust SQLite More Than Yourself
|
||||
|
||||
Let's put things in the right order. An SQLite database stored on a user's device is more important than the Swift code that accesses it. When a user installs a new version of an application, only the database stored on the user's device remains the same. But all the Swift code may have changed.
|
||||
|
||||
This is why it is recommended to define a **robust database schema** even before playing with record types.
|
||||
|
||||
This is important because SQLite is very robust, whereas we developers write bugs. The more responsibility we give to SQLite, the less code we have to write, and the fewer defects we will ship on our users' devices, affecting their precious data.
|
||||
|
||||
For example, if we were to define <doc:Migrations> that configure a database made of books and their authors, we could write:
|
||||
|
||||
```swift
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
migrator.registerMigration("createLibrary") { db in
|
||||
try db.create(table: "author") { t in // (1)
|
||||
t.autoIncrementedPrimaryKey("id") // (2)
|
||||
t.column("name", .text).notNull() // (3)
|
||||
t.column("countryCode", .text) // (4)
|
||||
}
|
||||
|
||||
try db.create(table: "book") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("title", .text).notNull() // (5)
|
||||
t.belongsTo("author", onDelete: .cascade) // (6)
|
||||
.notNull() // (7)
|
||||
}
|
||||
}
|
||||
|
||||
try migrator.migrate(dbQueue)
|
||||
```
|
||||
|
||||
1. Our database tables follow the <doc:DatabaseSchema#Database-Schema-Recommendations>: table names are English, singular, and camelCased. They look like Swift identifiers: `author`, `book`, `postalAddress`, `httpRequest`.
|
||||
2. Each author has a unique id.
|
||||
3. An author must have a name.
|
||||
4. The country of an author is not always known.
|
||||
5. A book must have a title.
|
||||
6. The `book.authorId` column is used to link a book to the author it belongs to. This column is indexed in order to ease the selection of an author's books. A foreign key is defined from `book.authorId` column to `authors.id`, so that SQLite guarantees that no book refers to a missing author. The `onDelete: .cascade` option has SQLite automatically delete all of an author's books when that author is deleted. See [Foreign Key Actions](https://sqlite.org/foreignkeys.html#fk_actions) for more information.
|
||||
7. The `book.authorId` column is not null so that SQLite guarantees that all books have an author.
|
||||
|
||||
Thanks to this database schema, the application will always process *consistent data*, no matter how wrong the Swift code can get. Even after a hard crash, all books will have an author, a non-nil title, etc.
|
||||
|
||||
> Tip: **A local SQLite database is not a JSON payload loaded from a remote server.**
|
||||
>
|
||||
> The JSON format and content can not be controlled, and an application must defend itself against wacky servers. But a local database is under your full control. It is trustable. A relational database such as SQLite guarantees the quality of users data, as long as enough energy is put in the proper definition of the database schema.
|
||||
|
||||
> Tip: **Plan early for future versions of your application**: use <doc:Migrations>.
|
||||
|
||||
## Record Types
|
||||
|
||||
### Persistable Record Types are Responsible for Their Tables
|
||||
|
||||
**Define one record type per database table.** This record type will be responsible for writing in this table.
|
||||
|
||||
**Let's start from regular structs** whose properties match the columns in their database table. They conform to the standard [`Codable`] protocol so that we don't have to write the methods that convert to and from raw database rows.
|
||||
|
||||
```swift
|
||||
struct Author: Codable {
|
||||
var id: Int64?
|
||||
var name: String
|
||||
var countryCode: String?
|
||||
}
|
||||
|
||||
struct Book: Codable {
|
||||
var id: Int64?
|
||||
var authorId: Int64
|
||||
var title: String
|
||||
}
|
||||
```
|
||||
|
||||
**We add database powers to our types with record protocols.**
|
||||
|
||||
The `author` and `book` tables have an auto-incremented id. We want inserted records to learn about their id after a successful insertion. That's why we have them conform to the ``MutablePersistableRecord`` protocol, and implement ``MutablePersistableRecord/didInsert(_:)-109jm``. Other kinds of record types would just use ``PersistableRecord``, and ignore `didInsert`.
|
||||
|
||||
On the reading side, we use ``FetchableRecord``, the protocol that can decode database rows.
|
||||
|
||||
This gives:
|
||||
|
||||
```swift
|
||||
// Add Database access
|
||||
extension Author: FetchableRecord, MutablePersistableRecord {
|
||||
// Update auto-incremented id upon successful insertion
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
id = inserted.rowID
|
||||
}
|
||||
}
|
||||
|
||||
extension Book: FetchableRecord, MutablePersistableRecord {
|
||||
// Update auto-incremented id upon successful insertion
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
id = inserted.rowID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That's it. The `Author` type can read and write in the `author` database table. `Book` as well, in `book`:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Insert and set author's id
|
||||
var author = Author(name: "Herman Melville", countryCode: "US")
|
||||
try author.insert(db)
|
||||
|
||||
// Insert and set book's id
|
||||
var book = Book(authorId: author.id!, title: "Moby-Dick")
|
||||
try book.insert(db)
|
||||
}
|
||||
|
||||
let books = try dbQueue.read { db in
|
||||
try Book.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
> Tip: When a column of a database table can't be NULL, define a non-optional property in the record type. On the other side, when the database may contain NULL, define an optional property. Compare:
|
||||
>
|
||||
> ```swift
|
||||
> try db.create(table: "author") { t in
|
||||
> t.autoIncrementedPrimaryKey("id")
|
||||
> t.column("name", .text).notNull() // Can't be NULL
|
||||
> t.column("countryCode", .text) // Can be NULL
|
||||
> }
|
||||
>
|
||||
> struct Author: Codable {
|
||||
> var id: Int64?
|
||||
> var name: String // Not optional
|
||||
> var countryCode: String? // Optional
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> There are exceptions to this rule.
|
||||
>
|
||||
> For example, the `id` column is never NULL in the database. And yet, `Author` as an optional `id` property. That is because we want to create instances of `Author` before they could be inserted in the database, and be assigned an auto-incremented id. If the `id` property was not optional, the `Author` type could not profit from auto-incremented ids!
|
||||
>
|
||||
> Another exception to this rule is described in <doc:RecordTimestamps>, where the creation date of a record is never NULL in the database, but optional in the Swift type.
|
||||
|
||||
> Tip: When the database table has a single-column primary key, have the record type adopt the standard [`Identifiable`] protocol. This allows GRDB to define extra methods based on record ids:
|
||||
>
|
||||
> ```swift
|
||||
> let authorID: Int64 = 42
|
||||
> let author: Author = try dbQueue.read { db in
|
||||
> try Author.find(db, id: authorID)
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Take care that **`Identifiable` is not a good fit for optional ids**. You will frequently meet optional ids for records with auto-incremented ids:
|
||||
>
|
||||
> ```swift
|
||||
> struct Player: Codable {
|
||||
> var id: Int64? // Optional ids are not suitable for Identifiable
|
||||
> var name: String
|
||||
> var score: Int
|
||||
> }
|
||||
>
|
||||
> extension Player: FetchableRecord, MutablePersistableRecord {
|
||||
> // Update auto-incremented id upon successful insertion
|
||||
> mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
> id = inserted.rowID
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> For more details about auto-incremented ids and `Identifiable`, see [issue #1435](https://github.com/groue/GRDB.swift/issues/1435#issuecomment-1740857712).
|
||||
|
||||
### Record Types Hide Intimate Database Details
|
||||
|
||||
In the previous sample codes, the `Book` and `Author` structs have one property per database column, and their types are natively supported by SQLite (`String`, `Int`, etc.)
|
||||
|
||||
But it happens that raw database column names, or raw column types, are not a very good fit for the application.
|
||||
|
||||
When this happens, it's time to **distinguish the Swift and database representations**. Record types are the dedicated place where raw database values can be transformed into Swift types that are well-suited for the rest of the application.
|
||||
|
||||
Let's look at three examples.
|
||||
|
||||
#### First Example: Enums
|
||||
|
||||
Authors write books, and more specifically novels, poems, essays, or theatre plays. Let's add a `kind` column in the database. We decide that a book kind is represented as a string ("novel", "essay", etc.) in the database:
|
||||
|
||||
```swift
|
||||
try db.create(table: "book") { t in
|
||||
...
|
||||
t.column("kind", .text).notNull()
|
||||
}
|
||||
```
|
||||
|
||||
In Swift, it is not a good practice to use `String` for the type of the `kind` property. We prefer an enum instead:
|
||||
|
||||
```swift
|
||||
struct Book: Codable {
|
||||
enum Kind: String, Codable {
|
||||
case essay, novel, poetry, theater
|
||||
}
|
||||
var id: Int64?
|
||||
var authorId: Int64
|
||||
var title: String
|
||||
var kind: Kind
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to its enum property, the `Book` record prevents invalid book kinds from being stored into the database.
|
||||
|
||||
In order to use `Book.Kind` in database requests for books (see <doc:RecordRecommendedPractices#Record-Requests> below), we add the ``DatabaseValueConvertible`` conformance to `Book.Kind`:
|
||||
|
||||
```swift
|
||||
extension Book.Kind: DatabaseValueConvertible { }
|
||||
|
||||
// Fetch all novels
|
||||
let novels = try dbQueue.read { db in
|
||||
try Book.filter(Column("kind") == Book.Kind.novel).fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
#### Second Example: GPS Coordinates
|
||||
|
||||
GPS coordinates can be stored in two distinct `latitude` and `longitude` columns. But the standard way to deal with such coordinate is a single `CLLocationCoordinate2D` struct.
|
||||
|
||||
When this happens, keep column properties private, and provide sensible accessors instead:
|
||||
|
||||
```swift
|
||||
try db.create(table: "place") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("latitude", .double).notNull()
|
||||
t.column("longitude", .double).notNull()
|
||||
}
|
||||
|
||||
struct Place: Codable {
|
||||
var id: Int64?
|
||||
var name: String
|
||||
private var latitude: CLLocationDegrees
|
||||
private var longitude: CLLocationDegrees
|
||||
|
||||
var coordinate: CLLocationCoordinate2D {
|
||||
get {
|
||||
CLLocationCoordinate2D(
|
||||
latitude: latitude,
|
||||
longitude: longitude)
|
||||
}
|
||||
set {
|
||||
latitude = newValue.latitude
|
||||
longitude = newValue.longitude
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Generally speaking, private properties make it possible to hide raw columns from the rest of the application. The next example shows another application of this technique.
|
||||
|
||||
#### Third Example: Money Amounts
|
||||
|
||||
Before storing money amounts in an SQLite database, take care that [floating-point numbers are never a good fit](https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency).
|
||||
|
||||
SQLite only supports two kinds of numbers: integers and doubles, so we'll store amounts as integers. $12.00 will be represented by 1200, a quantity of cents. This allows SQLite to compute exact sums of price, for example.
|
||||
|
||||
On the other side, an amount of cents is not very practical for the rest of the Swift application. The [`Decimal`] type looks like a better fit.
|
||||
|
||||
That's why the `Product` record type has a `price: Decimal` property, backed by a `priceCents` integer column:
|
||||
|
||||
```swift
|
||||
try db.create(table: "product") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("name", .text).notNull()
|
||||
t.column("priceCents", .integer).notNull()
|
||||
}
|
||||
|
||||
struct Product: Codable {
|
||||
var id: Int64?
|
||||
var name: String
|
||||
private var priceCents: Int
|
||||
|
||||
var price: Decimal {
|
||||
get {
|
||||
Decimal(priceCents) / 100
|
||||
}
|
||||
set {
|
||||
priceCents = Self.cents(for: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private static func cents(for value: Decimal) -> Int {
|
||||
Int(Double(truncating: NSDecimalNumber(decimal: value * 100)))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Record Requests
|
||||
|
||||
Once we have record types that are able to read and write in the database, we'd like to perform database requests of such records.
|
||||
|
||||
### Columns
|
||||
|
||||
Requests that filter or sort records are defined with **columns**, defined in a dedicated enumeration. When the record type conforms to [`Codable`], columns can be derived from the `CodingKeys` enum:
|
||||
|
||||
```swift
|
||||
// HOW TO define columns for a Codable record
|
||||
extension Author {
|
||||
enum Columns {
|
||||
static let id = Column(CodingKeys.id)
|
||||
static let name = Column(CodingKeys.name)
|
||||
static let countryCode = Column(CodingKeys.countryCode)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For other record types, declare a plain `String` enum that conforms to the ``ColumnExpression`` protocol:
|
||||
|
||||
```swift
|
||||
// HOW TO define columns for a non-Codable record
|
||||
extension Author {
|
||||
enum Columns: String, ColumnExpression {
|
||||
case id, name, countryCode
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
From those columns it is possible to define requests of type ``QueryInterfaceRequest``:
|
||||
|
||||
```swift
|
||||
try dbQueue.read { db in
|
||||
// Fetch all authors, ordered by name,
|
||||
// in a localized case-insensitive fashion
|
||||
let sortedAuthors: [Author] = try Author.all()
|
||||
.order(Author.Columns.name.collating(.localizedCaseInsensitiveCompare))
|
||||
.fetchAll(db)
|
||||
|
||||
// Count French authors
|
||||
let frenchAuthorCount: Int = try Author.all()
|
||||
.filter(Author.Columns.countryCode == "FR")
|
||||
.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
### Turn Commonly-Used Requests into Methods
|
||||
|
||||
An application can define reusable request methods that extend the built-in GRDB apis. Those methods avoid code repetition, ease refactoring, and foster testability.
|
||||
|
||||
Define those methods in extensions of the ``DerivableRequest`` protocol, as below:
|
||||
|
||||
```swift
|
||||
// Author requests
|
||||
extension DerivableRequest<Author> {
|
||||
/// Order authors by name, in a localized case-insensitive fashion
|
||||
func orderByName() -> Self {
|
||||
let name = Author.Columns.name
|
||||
return order(name.collating(.localizedCaseInsensitiveCompare))
|
||||
}
|
||||
|
||||
/// Filters authors from a country
|
||||
func filter(countryCode: String) -> Self {
|
||||
filter(Author.Columns.countryCode == countryCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Book requests
|
||||
extension DerivableRequest<Book> {
|
||||
/// Order books by title, in a localized case-insensitive fashion
|
||||
func orderByTitle() -> Self {
|
||||
let title = Book.Columns.title
|
||||
return order(title.collating(.localizedCaseInsensitiveCompare))
|
||||
}
|
||||
|
||||
/// Filters books by kind
|
||||
func filter(kind: Book.Kind) -> Self {
|
||||
filter(Book.Columns.kind == kind)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Those methods define a fluent and legible api that encapsulates intimate database details:
|
||||
|
||||
```swift
|
||||
try dbQueue.read { db in
|
||||
let sortedSpanishAuthors: [Author] = try Author.all()
|
||||
.filter(countryCode: "ES")
|
||||
.orderByName()
|
||||
.fetchAll(db)
|
||||
|
||||
let novelCount: Int = try Book.all()
|
||||
.filter(kind: .novel)
|
||||
.fetchCount(db)
|
||||
}
|
||||
```
|
||||
|
||||
Extensions to the `DerivableRequest` protocol can not change the type of requests. They remain requests of the base record. To define requests of another type, use an extension to ``QueryInterfaceRequest``, as in the example below:
|
||||
|
||||
```swift
|
||||
extension QueryInterfaceRequest<Author> {
|
||||
// Selects author ids
|
||||
func selectId() -> QueryInterfaceRequest<Int64> {
|
||||
selectPrimaryKey(as: Int64.self)
|
||||
}
|
||||
}
|
||||
|
||||
// The ids of Japanese authors
|
||||
let ids: Set<Int64> = try Author.all()
|
||||
.filter(countryCode: "JP")
|
||||
.selectId()
|
||||
.fetchSet(db)
|
||||
```
|
||||
|
||||
## Associations
|
||||
|
||||
[Associations] help navigating from authors to their books and vice versa. Because the `book` table has an `authorId` column, we say that each book **belongs to** its author, and each author **has many** books:
|
||||
|
||||
```swift
|
||||
extension Book {
|
||||
static let author = belongsTo(Author.self)
|
||||
}
|
||||
|
||||
extension Author {
|
||||
static let books = hasMany(Book.self)
|
||||
}
|
||||
```
|
||||
|
||||
With associations, you can fetch a book's author, or an author's books:
|
||||
|
||||
```swift
|
||||
// Fetch all novels from an author
|
||||
try dbQueue.read { db in
|
||||
let author: Author = ...
|
||||
let novels: [Book] = try author.request(for: Author.books)
|
||||
.filter(kind: .novel)
|
||||
.orderByTitle()
|
||||
.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
Associations also make it possible to define more convenience request methods:
|
||||
|
||||
```swift
|
||||
extension DerivableRequest<Book> {
|
||||
/// Filters books from a country
|
||||
func filter(authorCountryCode countryCode: String) -> Self {
|
||||
// Books do not have any country column. But their author has one!
|
||||
// Return books that can be joined to an author from this country:
|
||||
joining(required: Book.author.filter(countryCode: countryCode))
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all Italian novels
|
||||
try dbQueue.read { db in
|
||||
let italianNovels: [Book] = try Book.all()
|
||||
.filter(kind: .novel)
|
||||
.filter(authorCountryCode: "IT")
|
||||
.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
With associations, you can also process graphs of authors and books, as described in the next section.
|
||||
|
||||
### How to Model Graphs of Objects
|
||||
|
||||
Since the beginning of this article, the `Book` and `Author` are independent structs that don't know each other. The only "meeting point" is the `Book.authorId` property.
|
||||
|
||||
Record types don't know each other on purpose: one does not need to know the author of a book when it's time to update the title of a book, for example.
|
||||
|
||||
When an application wants to process authors and books together, it defines dedicated types that model the desired view on the graph of related objects. For example:
|
||||
|
||||
```swift
|
||||
// Fetch all authors along with their number of books
|
||||
struct AuthorInfo: Decodable, FetchableRecord {
|
||||
var author: Author
|
||||
var bookCount: Int
|
||||
}
|
||||
let authorInfos: [AuthorInfo] = try dbQueue.read { db in
|
||||
try Author
|
||||
.annotated(with: Author.books.count)
|
||||
.asRequest(of: AuthorInfo.self)
|
||||
.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// Fetch the literary careers of German authors, sorted by name
|
||||
struct LiteraryCareer: Codable, FetchableRecord {
|
||||
var author: Author
|
||||
var books: [Book]
|
||||
}
|
||||
let careers: [LiteraryCareer] = try dbQueue.read { db in
|
||||
try Author
|
||||
.filter(countryCode: "DE")
|
||||
.orderByName()
|
||||
.including(all: Author.books)
|
||||
.asRequest(of: LiteraryCareer.self)
|
||||
.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// Fetch all Colombian books and their authors
|
||||
struct Authorship: Decodable, FetchableRecord {
|
||||
var book: Book
|
||||
var author: Author
|
||||
}
|
||||
let authorships: [Authorship] = try dbQueue.read { db in
|
||||
try Book.all()
|
||||
.including(required: Book.author.filter(countryCode: "CO"))
|
||||
.asRequest(of: Authorship.self)
|
||||
.fetchAll(db)
|
||||
|
||||
// Equivalent alternative
|
||||
try Book.all()
|
||||
.filter(authorCountryCode: "CO")
|
||||
.including(required: Book.author)
|
||||
.asRequest(of: Authorship.self)
|
||||
.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
In the above sample codes, requests that fetch values from several tables are decoded into additional record types: `AuthorInfo`, `LiteraryCareer`, and `Authorship`.
|
||||
|
||||
Those record type conform to both [`Decodable`] and ``FetchableRecord``, so that they can feed from database rows. They do not provide any persistence methods, though. **All database writes are performed from persistable record instances** (of type `Author` or `Book`).
|
||||
|
||||
For more information about associations, see the [Associations] guide.
|
||||
|
||||
### Lazy and Eager Loading: Comparison with Other Database Libraries
|
||||
|
||||
The additional record types described in the previous section may look superfluous. Some other database libraries are able to navigate in graphs of records without additional types.
|
||||
|
||||
For example, [Core Data] and Ruby's [Active Record] use **lazy loading**. This means that relationships are lazily fetched on demand:
|
||||
|
||||
```ruby
|
||||
# Lazy loading with Active Record
|
||||
author = Author.first # Fetch first author
|
||||
puts author.name
|
||||
author.books.each do |book| # Lazily fetch books on demand
|
||||
puts book.title
|
||||
end
|
||||
```
|
||||
|
||||
**GRDB does not perform lazy loading.** In a GUI application, lazy loading can not be achieved without record management (as in [Core Data]), which in turn comes with non-trivial pain points for developers regarding concurrency. Instead of lazy loading, the library provides the tooling needed to fetch data, even complex graphs, in an [isolated] fashion, so that fetched values accurately represent the database content, and all database invariants are preserved. See the <doc:Concurrency> guide for more information.
|
||||
|
||||
Vapor [Fluent] uses **eager loading**, which means that relationships are only fetched if explicitly requested:
|
||||
|
||||
```swift
|
||||
// Eager loading with Fluent
|
||||
let query = Author.query(on: db)
|
||||
.with(\.$books) // <- Explicit request for books
|
||||
.first()
|
||||
|
||||
// Fetch first author and its books in one stroke
|
||||
if let author = query.get() {
|
||||
print(author.name)
|
||||
for book in author.books { print(book.title) }
|
||||
}
|
||||
```
|
||||
|
||||
One must take care of fetching relationships, though, or Fluent raises a fatal error:
|
||||
|
||||
```swift
|
||||
// Oops, the books relation is not explicitly requested
|
||||
let query = Author.query(on: db).first()
|
||||
if let author = query.get() {
|
||||
// fatal error: Children relation not eager loaded.
|
||||
for book in author.books { print(book.title) }
|
||||
}
|
||||
```
|
||||
|
||||
**GRDB supports eager loading**. The difference with Fluent is that the relationships are modelled in a dedicated record type that provides runtime safety:
|
||||
|
||||
```swift
|
||||
// Eager loading with GRDB
|
||||
struct LiteraryCareer: Codable, FetchableRecord {
|
||||
var author: Author
|
||||
var books: [Book]
|
||||
}
|
||||
|
||||
let request = Author.all()
|
||||
.including(all: Author.books) // <- Explicit request for books
|
||||
.asRequest(of: LiteraryCareer.self)
|
||||
|
||||
// Fetch first author and its books in one stroke
|
||||
if let career = try request.fetchOne(db) {
|
||||
print(career.author.name)
|
||||
for book in career.books { print(book.title) }
|
||||
}
|
||||
```
|
||||
|
||||
[Active Record]: http://guides.rubyonrails.org/active_record_basics.html
|
||||
[`Codable`]: https://developer.apple.com/documentation/swift/Codable
|
||||
[Core Data]: https://developer.apple.com/documentation/coredata
|
||||
[`Decimal`]: https://developer.apple.com/documentation/foundation/decimal
|
||||
[`Decodable`]: https://developer.apple.com/documentation/swift/Decodable
|
||||
[Django]: https://docs.djangoproject.com/en/4.2/topics/db/
|
||||
[Fluent]: https://docs.vapor.codes/fluent/overview/
|
||||
[`Identifiable`]: https://developer.apple.com/documentation/swift/identifiable
|
||||
[isolated]: https://en.wikipedia.org/wiki/Isolation_(database_systems)
|
||||
[Associations]: https://github.com/groue/GRDB.swift/blob/master/Documentation/AssociationsBasics.md
|
||||
@@ -0,0 +1,440 @@
|
||||
# Record Timestamps and Transaction Date
|
||||
|
||||
Learn how applications can save creation and modification dates of records.
|
||||
|
||||
## Overview
|
||||
|
||||
Some applications want to record creation and modification dates of database records. This article provides some advice and sample code that you can adapt for your specific needs.
|
||||
|
||||
> Note: Creation and modification dates can be automatically handled by [SQLite triggers](https://www.sqlite.org/lang_createtrigger.html). We'll explore a different technique, though.
|
||||
>
|
||||
> This is not an advice against triggers, and you won't feel hindered in any way if you prefer to use triggers. Still, consider:
|
||||
>
|
||||
> - A trigger does not suffer any exception, when some applications eventually want to fine-tune timestamps, or to perform migrations without touching timestamps.
|
||||
> - The current time, according to SQLite, is not guaranteed to be constant in a given transaction. This may create undesired timestamp variations. We'll see below how GRDB provides a date that is constant at any point during a transaction.
|
||||
> - The current time, according to SQLite, can't be controlled in tests and previews.
|
||||
|
||||
We'll start from this table and record type:
|
||||
|
||||
```swift
|
||||
try db.create(table: "player") { t in
|
||||
t.autoIncrementedPrimaryKey("id")
|
||||
t.column("creationDate", .datetime).notNull()
|
||||
t.column("modificationDate", .datetime).notNull()
|
||||
t.column("name", .text).notNull()
|
||||
t.column("score", .integer).notNull()
|
||||
}
|
||||
|
||||
struct Player {
|
||||
var id: Int64?
|
||||
var creationDate: Date?
|
||||
var modificationDate: Date?
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
```
|
||||
|
||||
See how the table has non-null dates, while the record has optional dates.
|
||||
|
||||
This is because we intend, in this article, to timestamp actual database operations. The `creationDate` property is the date of database insertion, and `modificationDate` is the date of last modification in the database. A new `Player` instance has no meaningful timestamp until it is saved, and this absence of information is represented with `nil`:
|
||||
|
||||
```swift
|
||||
// A new player has no timestamps.
|
||||
var player = Player(id: nil, name: "Arthur", score: 1000)
|
||||
player.id // nil, because never saved
|
||||
player.creationDate // nil, because never saved
|
||||
player.modificationDate // nil, because never saved
|
||||
|
||||
// After insertion, the player has timestamps.
|
||||
try dbQueue.write { db in
|
||||
try player.insert(db)
|
||||
}
|
||||
player.id // not nil
|
||||
player.creationDate // not nil
|
||||
player.modificationDate // not nil
|
||||
```
|
||||
|
||||
In the rest of the article, we'll address insertion first, then updates, and see a way to avoid those optional timestamps. The article ends with a sample protocol that your app may adapt and reuse.
|
||||
|
||||
- <doc:RecordTimestamps#Insertion-Timestamp>
|
||||
- <doc:RecordTimestamps#Modification-Timestamp>
|
||||
- <doc:RecordTimestamps#Dealing-with-Optional-Timestamps>
|
||||
- <doc:RecordTimestamps#Sample-code-TimestampedRecord>
|
||||
|
||||
## Insertion Timestamp
|
||||
|
||||
On insertion, the `Player` record should get fresh `creationDate` and `modificationDate`. The ``MutablePersistableRecord`` protocol provides the necessary tooling, with the ``MutablePersistableRecord/willInsert(_:)-1xfwo`` persistence callback. Before insertion, the record sets both its `creationDate` and `modificationDate`:
|
||||
|
||||
```swift
|
||||
extension Player: Encodable, MutablePersistableRecord {
|
||||
/// Sets both `creationDate` and `modificationDate` to the
|
||||
/// transaction date, if they are not set yet.
|
||||
mutating func willInsert(_ db: Database) throws {
|
||||
if creationDate == nil {
|
||||
creationDate = try db.transactionDate
|
||||
}
|
||||
if modificationDate == nil {
|
||||
modificationDate = try db.transactionDate
|
||||
}
|
||||
}
|
||||
|
||||
/// Update auto-incremented id upon successful insertion
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
id = inserted.rowID
|
||||
}
|
||||
}
|
||||
|
||||
try dbQueue.write { db in
|
||||
// An inserted record has both a creation and a modification date.
|
||||
var player = Player(name: "Arthur", score: 1000)
|
||||
try player.insert(db)
|
||||
player.creationDate // not nil
|
||||
player.modificationDate // not nil
|
||||
}
|
||||
```
|
||||
|
||||
The `willInsert` callback uses the ``Database/transactionDate`` instead of `Date()`. This has two advantages:
|
||||
|
||||
- Within a write transaction, all inserted players get the same timestamp:
|
||||
|
||||
```swift
|
||||
// All players have the same timestamp.
|
||||
try dbQueue.write { db in
|
||||
for var player in players {
|
||||
try player.insert(db)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- The transaction date can be configured with ``Configuration/transactionClock``, so that your tests and previews can control the date.
|
||||
|
||||
## Modification Timestamp
|
||||
|
||||
Let's now deal with updates. The `update` persistence method won't automatically bump the timestamp as the `insert` method does. We have to explicitly deal with the modification date:
|
||||
|
||||
```swift
|
||||
// Increment the player score (two different ways).
|
||||
try dbQueue.write { db in
|
||||
var player: Player
|
||||
|
||||
// Update all columns
|
||||
player.score += 1
|
||||
player.modificationDate = try db.transactionDate
|
||||
try player.update(db)
|
||||
|
||||
// Alternatively, update only the modified columns
|
||||
try player.updateChanges(db) {
|
||||
$0.score += 1
|
||||
$0.modificationDate = try db.transactionDate
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Again, we use ``Database/transactionDate``, so that all modified players get the same timestamp within a given write transaction.
|
||||
|
||||
> Note: The insertion case could profit from automatic initialization of the creation date with the ``MutablePersistableRecord/willInsert(_:)-1xfwo`` persistence callback, but the modification date is not handled with ``MutablePersistableRecord/willUpdate(_:columns:)-3oko4``. Instead, the above sample code explicitly modifies the modification date.
|
||||
>
|
||||
> This may look like an inconvenience, but there are several reasons for this:
|
||||
>
|
||||
> 1. The persistence methods that update are not mutating methods. `willUpdate` can not modify the modification date.
|
||||
>
|
||||
> 2. Automatic changes to the modification date from the general `update` method create problems.
|
||||
>
|
||||
> Developers are seduced by this convenient-looking feature, but they also eventually want to disable automatic timestamp updates in specific circumstances. That's because application requirements happen to change, and developers happen to overlook some corner cases.
|
||||
>
|
||||
> This need is well acknowledged by existing database libraries: to disable automatic timestamp updates, [ActiveRecord](https://stackoverflow.com/questions/861448/is-there-a-way-to-avoid-automatically-updating-rails-timestamp-fields) uses globals (not thread-safe in a Swift application), [Django ORM](https://stackoverflow.com/questions/7499767/temporarily-disable-auto-now-auto-now-add) does not make it easy, and [Fluent](https://github.com/vapor/fluent-kit/issues/355) simply does not allow it.
|
||||
>
|
||||
> None of those solutions or lack thereof are seducing.
|
||||
>
|
||||
> 3. Not all applications need one modification timestamp. For example, some need one timestamp per property, or per group of properties.
|
||||
>
|
||||
> By not providing automatic timestamp updates, all GRDB-powered applications are treated equally: they explicitly bump their modification timestamps when needed. Apps can help themselves by introducing protocols dedicated to their particular handling of updates. For an example of such a protocol, see <doc:RecordTimestamps#Sample-code-TimestampedRecord> below.
|
||||
|
||||
## Dealing with Optional Timestamps
|
||||
|
||||
When you fetch timestamped records from the database, it may be inconvenient to deal with optional dates, even though the database columns are guaranteed to be not null:
|
||||
|
||||
```swift
|
||||
let player = try dbQueue.read { db
|
||||
try Player.find(db, key: 1)
|
||||
}
|
||||
player.creationDate // optional 😕
|
||||
player.modificationDate // optional 😕
|
||||
```
|
||||
|
||||
A possible technique is to define two record types: one that deals with players in general (optional timestamps), and one that only deals with persisted players (non-optional dates):
|
||||
|
||||
```swift
|
||||
/// `Player` deals with unsaved players
|
||||
struct Player {
|
||||
var id: Int64? // optional
|
||||
var creationDate: Date? // optional
|
||||
var modificationDate: Date? // optional
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
|
||||
extension Player: Encodable, MutablePersistableRecord {
|
||||
/// Updates auto-incremented id upon successful insertion
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
id = inserted.rowID
|
||||
}
|
||||
|
||||
/// Sets both `creationDate` and `modificationDate` to the
|
||||
/// transaction date, if they are not set yet.
|
||||
mutating func willInsert(_ db: Database) throws {
|
||||
if creationDate == nil {
|
||||
creationDate = try db.transactionDate
|
||||
}
|
||||
if modificationDate == nil {
|
||||
modificationDate = try db.transactionDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `PersistedPlayer` deals with persisted players
|
||||
struct PersistedPlayer: Identifiable {
|
||||
let id: Int64 // not optional
|
||||
let creationDate: Date // not optional
|
||||
var modificationDate: Date // not optional
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
|
||||
extension PersistedPlayer: Codable, FetchableRecord, PersistableRecord {
|
||||
static var databaseTableName: String { "player" }
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```swift
|
||||
// Fetch
|
||||
try dbQueue.read { db
|
||||
let persistedPlayer = try PersistedPlayer.find(db, id: 1)
|
||||
persistedPlayer.creationDate // not optional
|
||||
persistedPlayer.modificationDate // not optional
|
||||
}
|
||||
|
||||
// Insert
|
||||
try dbQueue.write { db in
|
||||
var player = Player(id: nil, name: "Arthur", score: 1000)
|
||||
player.id // nil
|
||||
player.creationDate // nil
|
||||
player.modificationDate // nil
|
||||
|
||||
let persistedPlayer = try player.insertAndFetch(db, as: PersistedPlayer.self)
|
||||
persistedPlayer.id // not optional
|
||||
persistedPlayer.creationDate // not optional
|
||||
persistedPlayer.modificationDate // not optional
|
||||
}
|
||||
```
|
||||
|
||||
See ``MutablePersistableRecord/insertAndFetch(_:onConflict:as:)`` and related methods for more information.
|
||||
|
||||
## Sample code: TimestampedRecord
|
||||
|
||||
This section provides a sample protocol for records that track their creation and modification dates.
|
||||
|
||||
You can copy it in your application, or use it as an inspiration. Not all apps have the same needs regarding timestamps!
|
||||
|
||||
`TimestampedRecord` provides the following features and methods:
|
||||
|
||||
- Use it as a replacement for `MutablePersistableRecord` (even if your record does not use an auto-incremented primary key):
|
||||
|
||||
```swift
|
||||
// The base Player type
|
||||
struct Player {
|
||||
var id: Int64?
|
||||
var creationDate: Date?
|
||||
var modificationDate: Date?
|
||||
var name: String
|
||||
var score: Int
|
||||
}
|
||||
|
||||
// Add database powers (read, write, timestamps)
|
||||
extension Player: Codable, TimestampedRecord, FetchableRecord {
|
||||
/// Update auto-incremented id upon successful insertion
|
||||
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||
id = inserted.rowID
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Timestamps are set on insertion:
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// An inserted record has both a creation and a modification date.
|
||||
var player = Player(name: "Arthur", score: 1000)
|
||||
try player.insert(db)
|
||||
player.creationDate // not nil
|
||||
player.modificationDate // not nil
|
||||
}
|
||||
```
|
||||
|
||||
- `updateWithTimestamp()` behaves like ``MutablePersistableRecord/update(_:onConflict:)``, but it also bumps the modification date.
|
||||
|
||||
```swift
|
||||
// Bump the modification date and update all columns in the database.
|
||||
player.score += 1
|
||||
try player.updateWithTimestamp(db)
|
||||
```
|
||||
|
||||
- `updateChangesWithTimestamp()` behaves like ``MutablePersistableRecord/updateChanges(_:onConflict:modify:)``, but it also bumps the modification date if the record is modified.
|
||||
|
||||
```swift
|
||||
// Only bump the modification date if record is changed, and only
|
||||
// update the changed columns.
|
||||
try player.updateChangesWithTimestamp(db) {
|
||||
$0.score = 1000
|
||||
}
|
||||
|
||||
// Prefer updateChanges() if the modification date should always be
|
||||
// updated, even if other columns are not changed.
|
||||
try player.updateChanges(db) {
|
||||
$0.score = 1000
|
||||
$0.modificationDate = try db.transactionDate
|
||||
}
|
||||
```
|
||||
|
||||
- `touch()` only updates the modification date in the database, just like the `touch` unix command.
|
||||
|
||||
```swift
|
||||
// Only update the modification date in the database.
|
||||
try player.touch(db)
|
||||
```
|
||||
|
||||
- There is no `TimestampedRecord.saveWithTimestamp()` method that would insert or update, like ``MutablePersistableRecord/save(_:onConflict:)``. You are encouraged to write instead (and maybe extend your version of `TimestampedRecord` so that it supports this pattern):
|
||||
|
||||
```swift
|
||||
extension Player {
|
||||
/// If the player has a non-nil primary key and a matching row in
|
||||
/// the database, the player is updated. Otherwise, it is inserted.
|
||||
mutating func saveWithTimestamp(_ db: Database) throws {
|
||||
// Test the presence of id first, so that we don't perform an
|
||||
// update that would surely throw RecordError.recordNotFound.
|
||||
if id == nil {
|
||||
try insert(db)
|
||||
} else {
|
||||
do {
|
||||
try updateWithTimestamp(db)
|
||||
} catch RecordError.recordNotFound {
|
||||
// Primary key is set, but no row was updated.
|
||||
try insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The full implementation of `TimestampedRecord` follows:
|
||||
|
||||
```swift
|
||||
/// A record type that tracks its creation and modification dates. See
|
||||
/// <https://swiftpackageindex.com/groue/grdb.swift/documentation/grdb/recordtimestamps>
|
||||
protocol TimestampedRecord: MutablePersistableRecord {
|
||||
var creationDate: Date? { get set }
|
||||
var modificationDate: Date? { get set }
|
||||
}
|
||||
|
||||
extension TimestampedRecord {
|
||||
/// By default, `TimestampedRecord` types set `creationDate` and
|
||||
/// `modificationDate` to the transaction date, if they are nil,
|
||||
/// before insertion.
|
||||
///
|
||||
/// `TimestampedRecord` types that customize the `willInsert`
|
||||
/// persistence callback should call `initializeTimestamps` from
|
||||
/// their implementation.
|
||||
mutating func willInsert(_ db: Database) throws {
|
||||
try initializeTimestamps(db)
|
||||
}
|
||||
|
||||
/// Sets `creationDate` and `modificationDate` to the transaction date,
|
||||
/// if they are nil.
|
||||
///
|
||||
/// It is called automatically before insertion, if your type does not
|
||||
/// customize the `willInsert` persistence callback. If you customize
|
||||
/// this callback, call `initializeTimestamps` from your implementation.
|
||||
mutating func initializeTimestamps(_ db: Database) throws {
|
||||
if creationDate == nil {
|
||||
creationDate = try db.transactionDate
|
||||
}
|
||||
if modificationDate == nil {
|
||||
modificationDate = try db.transactionDate
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets `modificationDate`, and executes an `UPDATE` statement
|
||||
/// on all columns.
|
||||
///
|
||||
/// - parameter modificationDate: The modification date. If nil, the
|
||||
/// transaction date is used.
|
||||
mutating func updateWithTimestamp(_ db: Database, modificationDate: Date? = nil) throws {
|
||||
self.modificationDate = try modificationDate ?? db.transactionDate
|
||||
try update(db)
|
||||
}
|
||||
|
||||
/// Modifies the record according to the provided `modify` closure, and,
|
||||
/// if and only if the record was modified, sets `modificationDate` and
|
||||
/// executes an `UPDATE` statement that updates the modified columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// var player = Player.find(db, id: 1)
|
||||
/// let modified = try player.updateChangesWithTimestamp(db) {
|
||||
/// $0.score = 1000
|
||||
/// }
|
||||
/// if modified {
|
||||
/// print("player was modified")
|
||||
/// } else {
|
||||
/// print("player was not modified")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - modificationDate: The modification date. If nil, the
|
||||
/// transaction date is used.
|
||||
/// - modify: A closure that modifies the record.
|
||||
/// - returns: Whether the record was changed and updated.
|
||||
@discardableResult
|
||||
mutating func updateChangesWithTimestamp(
|
||||
_ db: Database,
|
||||
modificationDate: Date? = nil,
|
||||
modify: (inout Self) -> Void)
|
||||
throws -> Bool
|
||||
{
|
||||
// Grab the changes performed by `modify`
|
||||
let initialChanges = try databaseChanges(modify: modify)
|
||||
if initialChanges.isEmpty {
|
||||
return false
|
||||
}
|
||||
|
||||
// Update modification date and grab its column name
|
||||
let dateChanges = try databaseChanges(modify: {
|
||||
$0.modificationDate = try modificationDate ?? db.transactionDate
|
||||
})
|
||||
|
||||
// Update the modified columns
|
||||
let modifiedColumns = Set(initialChanges.keys).union(dateChanges.keys)
|
||||
try update(db, columns: modifiedColumns)
|
||||
return true
|
||||
}
|
||||
|
||||
/// Sets `modificationDate`, and executes an `UPDATE` statement that
|
||||
/// updates the `modificationDate` column, if and only if the record
|
||||
/// was modified.
|
||||
///
|
||||
/// - parameter modificationDate: The modification date. If nil, the
|
||||
/// transaction date is used.
|
||||
mutating func touch(_ db: Database, modificationDate: Date? = nil) throws {
|
||||
try updateChanges(db) {
|
||||
$0.modificationDate = try modificationDate ?? db.transactionDate
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 124 KiB |