add iOS
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
import Dispatch
|
||||
import Foundation
|
||||
|
||||
public struct Configuration {
|
||||
|
||||
// MARK: - Misc options
|
||||
|
||||
/// A boolean value indicating whether foreign key support is enabled.
|
||||
///
|
||||
/// The default is true.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/foreignkeys.html>.
|
||||
public var foreignKeysEnabled = true
|
||||
|
||||
/// A boolean value indicating whether an SQLite connection is read-only.
|
||||
///
|
||||
/// The default is false.
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.readonly = true
|
||||
///
|
||||
/// let dbQueue = try DatabaseQueue( // or DatabasePool
|
||||
/// path: "/path/to/database.sqlite",
|
||||
/// configuration: config)
|
||||
/// ```
|
||||
public var readonly = false
|
||||
|
||||
/// A label that describes a database connection.
|
||||
///
|
||||
/// You can query this label at runtime:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.label = "MyDatabase"
|
||||
/// let dbQueue = try DatabaseQueue(configuration: config)
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// print(db.configuration.label) // Prints "MyDatabase"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The configuration label is also used to name ``Database`` connections
|
||||
/// (their ``Database/description`` property), and the various dispatch
|
||||
/// queues created by GRDB, visible in debugging sessions and crash logs.
|
||||
///
|
||||
/// Those connection names and dispatch queue labels are intended for
|
||||
/// debugging only. Their format may change between GRDB releases.
|
||||
/// Applications should not depend on connection names and dispatch
|
||||
/// queue labels.
|
||||
///
|
||||
/// If the configuration label is nil, the current GRDB implementation uses
|
||||
/// the following names:
|
||||
///
|
||||
/// - `GRDB.DatabaseQueue`: the (unique) connection of a DatabaseQueue
|
||||
/// - `GRDB.DatabasePool.writer`: the (unique) writer connection of
|
||||
/// a DatabasePool
|
||||
/// - `GRDB.DatabasePool.reader.N`, where N is 1, 2, ...: one of the reader
|
||||
/// connection(s) of a DatabasePool. N may get bigger than the maximum
|
||||
/// number of concurrent readers, as SQLite connections get closed and new
|
||||
/// ones are opened.
|
||||
/// - `GRDB.DatabasePool.snapshot.N`: the connection of a DatabaseSnapshot.
|
||||
/// N grows with the number of snapshots.
|
||||
///
|
||||
/// If the configuration label is not nil, for example "MyDatabase", the
|
||||
/// current GRDB implementation uses the following names:
|
||||
///
|
||||
/// - `MyDatabase`: the (unique) connection of a DatabaseQueue
|
||||
/// - `MyDatabase.writer`: the (unique) writer connection of a DatabasePool
|
||||
/// - `MyDatabase.reader.N`, where N is 1, 2, ...: one of the reader
|
||||
/// connection(s) of a DatabasePool. N may get bigger than the maximum
|
||||
/// number of concurrent readers, as SQLite connections get closed and new
|
||||
/// ones are opened.
|
||||
/// - `MyDatabase.snapshot.N`: the connection of a DatabaseSnapshot. N grows
|
||||
/// with the number of snapshots.
|
||||
///
|
||||
/// The default configuration label is nil.
|
||||
public var label: String? = nil
|
||||
|
||||
/// A boolean value indicating whether SQLite 3.29+ interprets
|
||||
/// double-quoted strings as string literals when they does not match any
|
||||
/// valid identifier.
|
||||
///
|
||||
/// The default and recommended value is false:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Error: no such column: missingColumn
|
||||
/// let name = try String.fetchOne(db, sql: """
|
||||
/// SELECT "missingColumn" FROM "player"
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// When true, or before SQLite version 3.29.0, double-quoted strings that
|
||||
/// do not match any valid identifier are interpreted as string literals,
|
||||
/// as in the example below. This is an SQLite
|
||||
/// [misfeature](https://sqlite.org/quirks.html#double_quoted_string_literals_are_accepted):
|
||||
///
|
||||
/// ```swift
|
||||
/// // MISFEATURE: This query succeeds with result "missingColumn"
|
||||
/// let name = try String.fetchOne(db, sql: """
|
||||
/// SELECT "missingColumn" FROM "player"
|
||||
/// """)
|
||||
/// ```
|
||||
public var acceptsDoubleQuotedStringLiterals = false
|
||||
|
||||
/// A boolean value indicating whether the database connection listens to
|
||||
/// the ``Database/suspendNotification`` and ``Database/resumeNotification``
|
||||
/// notifications.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// Set this flag to true when you apply the technique described in
|
||||
/// <doc:DatabaseSharing#How-to-limit-the-0xDEAD10CC-exception>. See
|
||||
/// ``Database/suspendNotification`` for more informations about
|
||||
/// suspended databases.
|
||||
public var observesSuspensionNotifications = false
|
||||
|
||||
/// A boolean value indicating whether statement arguments are visible in
|
||||
/// the description of database errors and trace events.
|
||||
///
|
||||
/// The default and recommended value is false: statement arguments are not
|
||||
/// visible in database errors and trace events, preventing sensitive
|
||||
/// information from leaking in unexpected places. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// db.trace { event in
|
||||
/// // By default, sensitive information is NOT printed
|
||||
/// // when a statement is traced:
|
||||
/// print(event)
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// // The sensitive information to protect
|
||||
/// let email = "..."
|
||||
/// let player = try Player.filter(Column("email") == email).fetchOne(db)
|
||||
/// } catch {
|
||||
/// // By default, sensitive information is NOT printed
|
||||
/// // when an error occurs:
|
||||
/// print(error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For debugging purpose, you can set this flag to true, and get more
|
||||
/// precise database reports. It is your responsibility to prevent sensitive
|
||||
/// information from leaking in unexpected locations, so you should not set
|
||||
/// this flag in release builds (think about GDPR and other
|
||||
/// privacy-related rules):
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// #if DEBUG
|
||||
/// // Enable verbose debugging in DEBUG builds only
|
||||
/// config.publicStatementArguments = true
|
||||
/// #endif
|
||||
///
|
||||
/// db.trace { event in
|
||||
/// // Sensitive information is printed in DEBUG builds:
|
||||
/// print(event)
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// // The sensitive information to protect
|
||||
/// let email = "..."
|
||||
/// let player = try Player.filter(Column("email") == email).fetchOne(db)
|
||||
/// } catch {
|
||||
/// // Sensitive information is printed in DEBUG builds:
|
||||
/// print(error)
|
||||
/// }
|
||||
/// ```
|
||||
public var publicStatementArguments = false
|
||||
|
||||
/// The clock that feeds ``Database/transactionDate``.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// The default clock is ``DefaultTransactionClock`` (which returns the
|
||||
/// start date of the current transaction).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.transactionClock = .custom { db in /* return some Date */ }
|
||||
/// ```
|
||||
public var transactionClock: any TransactionClock = .default
|
||||
|
||||
// MARK: - Managing SQLite Connections
|
||||
|
||||
private var setups: [(Database) throws -> Void] = []
|
||||
|
||||
/// Defines a function to run whenever an SQLite connection is opened.
|
||||
///
|
||||
/// The preparation function is run before the connection is made available
|
||||
/// for database access methods.
|
||||
///
|
||||
/// This method can be called several times. The preparation functions are
|
||||
/// run in the same order.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.prepareDatabase { db in
|
||||
/// // Prints all SQL statements
|
||||
/// db.trace { print("SQL >", $0) }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When you use a ``DatabasePool``, preparation functions are called for
|
||||
/// the writer connection and all reader connections. You can distinguish
|
||||
/// them by querying `db.configuration.readonly`:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.prepareDatabase { db in
|
||||
/// if db.configuration.readonly {
|
||||
/// // reader connection
|
||||
/// } else {
|
||||
/// // writer connection
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// On newly created databases files, ``DatabasePool`` activates the WAL
|
||||
/// mode after the preparation functions have run.
|
||||
public mutating func prepareDatabase(_ setup: @escaping (Database) throws -> Void) {
|
||||
setups.append(setup)
|
||||
}
|
||||
|
||||
// MARK: - Transactions
|
||||
|
||||
/// The default kind of write transactions.
|
||||
///
|
||||
/// The default is ``Database/TransactionKind/deferred``.
|
||||
///
|
||||
/// You can change the default transaction kind. For example, you can force
|
||||
/// all write transactions to be `IMMEDIATE`:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.defaultTransactionKind = .immediate
|
||||
/// let dbQueue = try DatabaseQueue(configuration: config)
|
||||
///
|
||||
/// // BEGIN IMMEDIATE TRANSACTION; ...; COMMIT TRANSACTION;
|
||||
/// try dbQueue.write { db in ... }
|
||||
/// ```
|
||||
///
|
||||
/// This property is ignored for read-only transactions. Those always open
|
||||
/// `DEFERRED` SQLite transactions.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/lang_transaction.html>
|
||||
public var defaultTransactionKind: Database.TransactionKind = .deferred
|
||||
|
||||
/// A boolean value indicating whether it is valid to leave a transaction
|
||||
/// opened at the end of a database access method.
|
||||
///
|
||||
/// The default value is false: not completing a transaction is a
|
||||
/// programmer error:
|
||||
///
|
||||
/// ```swift
|
||||
/// let dbQueue = try DatabaseQueue()
|
||||
///
|
||||
/// // fatal error: A transaction has been left opened at the end of a database access
|
||||
/// try dbQueue.inDatabase { db in
|
||||
/// try db.beginTransaction()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// When true, one can leave opened transaction at the end of database
|
||||
/// access method:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.allowsUnsafeTransactions = true
|
||||
/// let dbQueue = try DatabaseQueue(configuration: config)
|
||||
///
|
||||
/// try dbQueue.inDatabase { db in
|
||||
/// try db.beginTransaction()
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.inDatabase { db in
|
||||
/// try db.commit()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This configuration flag has no effect on ``DatabasePool`` reader
|
||||
/// connections: those never allow leaving a transaction opened at the end
|
||||
/// of a read access.
|
||||
public var allowsUnsafeTransactions = false
|
||||
|
||||
// MARK: - Journal Mode
|
||||
|
||||
/// Defines how the journal mode is configured when the database
|
||||
/// connection is opened.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
|
||||
public enum JournalModeConfiguration: Sendable {
|
||||
/// The default setup has ``DatabaseQueue`` perform no specific
|
||||
/// configuration of the journal mode, and ``DatabasePool``
|
||||
/// configure the database for the WAL mode (just like the
|
||||
/// ``wal`` case).
|
||||
case `default`
|
||||
|
||||
/// The journal mode is set to WAL (plus extra configurations that
|
||||
/// make life easier with WAL databases).
|
||||
case wal
|
||||
}
|
||||
|
||||
/// Defines how the journal mode is configured when the database
|
||||
/// connection is opened.
|
||||
///
|
||||
/// This configuration is ignored when ``readonly`` is true.
|
||||
///
|
||||
/// The default value has ``DatabaseQueue`` perform no specific
|
||||
/// configuration of the journal mode, and ``DatabasePool`` configure
|
||||
/// the database for the WAL mode.
|
||||
///
|
||||
/// Applications that need to open a WAL database with a
|
||||
/// ``DatabaseQueue`` should set the `journalMode` to `wal`:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Open a WAL database with DatabaseQueue
|
||||
/// var config = Configuration()
|
||||
/// config.journalMode = .wal
|
||||
/// let dbQueue = try DatabaseQueue(path: "...", configuration: config)
|
||||
/// ```
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/pragma.html#pragma_journal_mode>
|
||||
public var journalMode = JournalModeConfiguration.default
|
||||
|
||||
// MARK: - Concurrency
|
||||
|
||||
/// Defines the how `SQLITE_BUSY` errors are handled.
|
||||
///
|
||||
/// The default is ``Database/BusyMode/immediateError``.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html#busy>
|
||||
public var busyMode: Database.BusyMode = .immediateError
|
||||
|
||||
/// The behavior in case of SQLITE_BUSY error, for read-only connections.
|
||||
/// If nil, GRDB picks a default one.
|
||||
var readonlyBusyMode: Database.BusyMode? = nil
|
||||
|
||||
/// The maximum number of concurrent reader connections.
|
||||
///
|
||||
/// This configuration has effect on ``DatabasePool`` and
|
||||
/// ``DatabaseSnapshotPool`` only. The default value is 5.
|
||||
///
|
||||
/// You can query this value at runtime in order to get the actual capacity
|
||||
/// for concurrent reads of any ``DatabaseReader``. In this context,
|
||||
/// ``DatabaseQueue`` and ``DatabaseSnapshot`` have a capacity of 1,
|
||||
/// because they can't perform two concurrent reads. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// var config = Configuration()
|
||||
/// config.maximumReaderCount = 5
|
||||
///
|
||||
/// let path = "/path/to/database.sqlite"
|
||||
/// let dbQueue = try DatabaseQueue(path: path, configuration: config)
|
||||
/// let dbPool = try DatabasePool(path: path, configuration: config)
|
||||
/// let dbSnapshot = try dbPool.makeSnapshot()
|
||||
///
|
||||
/// print(dbQueue.configuration.maximumReaderCount) // 1
|
||||
/// print(dbPool.configuration.maximumReaderCount) // 5
|
||||
/// print(dbSnapshot.configuration.maximumReaderCount) // 1
|
||||
/// ```
|
||||
public var maximumReaderCount: Int = 5
|
||||
|
||||
/// The quality of service of database accesses.
|
||||
///
|
||||
/// The quality of service is ignored if you supply a ``targetQueue``.
|
||||
///
|
||||
/// The default is `userInitiated`.
|
||||
public var qos: DispatchQoS = .userInitiated
|
||||
|
||||
/// The effective quality of service of read-only database accesses.
|
||||
public var readQoS: DispatchQoS {
|
||||
targetQueue?.qos ?? self.qos
|
||||
}
|
||||
|
||||
/// The effective quality of service of write database accesses.
|
||||
public var writeQoS: DispatchQoS {
|
||||
writeTargetQueue?.qos ?? targetQueue?.qos ?? self.qos
|
||||
}
|
||||
|
||||
/// The target dispatch queue for database accesses.
|
||||
///
|
||||
/// Database connections which are not read-only will prefer
|
||||
/// ``writeTargetQueue`` instead, if it is not nil.
|
||||
///
|
||||
/// When you use ``DatabasePool``, make sure this queue is concurrent. This
|
||||
/// is because in a serial dispatch queue, no concurrent database access can
|
||||
/// happen, and you may experience deadlocks.
|
||||
///
|
||||
/// If the queue is nil, all database accesses happen in unspecified
|
||||
/// dispatch queues whose quality of service is determined by the
|
||||
/// ``qos`` property.
|
||||
///
|
||||
/// The default is nil.
|
||||
public var targetQueue: DispatchQueue? = nil
|
||||
|
||||
/// The target dispatch queue for write database accesses.
|
||||
///
|
||||
/// If this queue is nil, writer connections are controlled
|
||||
/// by ``targetQueue``.
|
||||
///
|
||||
/// The default is nil.
|
||||
public var writeTargetQueue: DispatchQueue? = nil
|
||||
|
||||
#if os(iOS)
|
||||
/// A boolean value indicating whether the database connection releases
|
||||
/// memory when entering the background or upon receiving a memory warning
|
||||
/// in iOS.
|
||||
///
|
||||
/// The default is true.
|
||||
public var automaticMemoryManagement = true
|
||||
#endif
|
||||
|
||||
/// A boolean value indicating whether read-only connections should be
|
||||
/// kept open.
|
||||
///
|
||||
/// This configuration flag applies to ``DatabasePool`` only. The
|
||||
/// default value is false.
|
||||
///
|
||||
/// When the flag is false, a `DatabasePool` closes read-only
|
||||
/// connections when requested to dispose non-essential memory with
|
||||
/// ``DatabasePool/releaseMemory()``. When true, those connections are
|
||||
/// kept open.
|
||||
///
|
||||
/// Consider setting this flag to true when profiling your application
|
||||
/// reveals that a lot of time is spent opening new SQLite connections.
|
||||
public var persistentReadOnlyConnections = false
|
||||
|
||||
// MARK: - Factory Configuration
|
||||
|
||||
/// Creates a factory configuration.
|
||||
public init() { }
|
||||
|
||||
// MARK: - Not Public
|
||||
|
||||
/// The SQLite [threading mode](https://www.sqlite.org/threadsafe.html).
|
||||
///
|
||||
/// - Note: Only the multi-thread mode (`SQLITE_OPEN_NOMUTEX`) is currently
|
||||
/// supported, since all <doc:DatabaseConnections> access SQLite connections
|
||||
/// through a `SerializedDatabase`.
|
||||
var threadingMode = Database.ThreadingMode.default
|
||||
|
||||
var SQLiteConnectionDidOpen: (() -> Void)?
|
||||
var SQLiteConnectionWillClose: ((SQLiteConnection) -> Void)?
|
||||
var SQLiteConnectionDidClose: (() -> Void)?
|
||||
var SQLiteOpenFlags: CInt {
|
||||
var flags = readonly ? SQLITE_OPEN_READONLY : (SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE)
|
||||
if sqlite3_libversion_number() >= 3037000 {
|
||||
flags |= 0x02000000 // SQLITE_OPEN_EXRESCODE
|
||||
}
|
||||
return threadingMode.SQLiteOpenFlags | flags
|
||||
}
|
||||
|
||||
func setUp(_ db: Database) throws {
|
||||
for f in setups {
|
||||
try f(db)
|
||||
}
|
||||
}
|
||||
|
||||
func identifier(defaultLabel: String, purpose: String? = nil) -> String {
|
||||
(self.label ?? defaultLabel) + (purpose.map { "." + $0 } ?? "")
|
||||
}
|
||||
|
||||
/// Creates a DispatchQueue which has the quality of service and target
|
||||
/// queue of write accesses.
|
||||
func makeWriterDispatchQueue(label: String) -> DispatchQueue {
|
||||
if let targetQueue = writeTargetQueue ?? targetQueue {
|
||||
return DispatchQueue(label: label, target: targetQueue)
|
||||
} else {
|
||||
return DispatchQueue(label: label, qos: qos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a DispatchQueue which has the quality of service and target
|
||||
/// queue of read accesses.
|
||||
func makeReaderDispatchQueue(label: String) -> DispatchQueue {
|
||||
if let targetQueue {
|
||||
return DispatchQueue(label: label, target: targetQueue)
|
||||
} else {
|
||||
return DispatchQueue(label: label, qos: qos)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,562 @@
|
||||
import Foundation
|
||||
|
||||
extension Database {
|
||||
|
||||
// MARK: - Statements
|
||||
|
||||
/// Returns a new prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.makeStatement(sql: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.makeStatement(sql: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func makeStatement(sql: String) throws -> Statement {
|
||||
try makeStatement(sql: sql, prepFlags: 0)
|
||||
}
|
||||
|
||||
/// Returns a new prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.makeStatement(literal: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.makeStatement(literal: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// In the provided literal, no argument must be set, or all arguments must
|
||||
/// be set. An error is raised otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try makeStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?
|
||||
/// """)
|
||||
/// try makeStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > \(1000)
|
||||
/// """)
|
||||
///
|
||||
/// // NOT OK (first argument is not set, but second is)
|
||||
/// try makeStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player
|
||||
/// WHERE color = ? AND score > \(1000)
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func makeStatement(literal sqlLiteral: SQL) throws -> Statement {
|
||||
let (sql, arguments) = try sqlLiteral.build(self)
|
||||
let statement = try makeStatement(sql: sql)
|
||||
if arguments.isEmpty == false {
|
||||
// Throws if arguments do not match
|
||||
try statement.setArguments(arguments)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns a new prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let statement = try db.makeStatement(sql: "SELECT COUNT(*) FROM player WHERE score > ?", prepFlags: 0)
|
||||
/// let moreThanTwentyCount = try Int.fetchOne(statement, arguments: [20])!
|
||||
/// let moreThanThirtyCount = try Int.fetchOne(statement, arguments: [30])!
|
||||
///
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - parameter prepFlags: Flags for sqlite3_prepare_v3 (available from
|
||||
/// SQLite 3.20.0, see <http://www.sqlite.org/c3ref/prepare.html>)
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
func makeStatement(sql: String, prepFlags: CUnsignedInt) throws -> Statement {
|
||||
let statements = SQLStatementCursor(database: self, sql: sql, arguments: nil, prepFlags: prepFlags)
|
||||
guard let statement = try statements.next() else {
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_ERROR,
|
||||
message: "empty statement",
|
||||
sql: sql)
|
||||
}
|
||||
do {
|
||||
guard try statements.next() == nil else {
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: """
|
||||
Multiple statements found. To execute multiple statements, use \
|
||||
Database.execute(sql:) or Database.allStatements(sql:) instead.
|
||||
""",
|
||||
sql: sql)
|
||||
}
|
||||
} catch {
|
||||
// Something while would not compile was found after the first statement.
|
||||
// Complain about multiple statements anyway.
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: """
|
||||
Multiple statements found. To execute multiple statements, use \
|
||||
Database.execute(sql:) or Database.allStatements(sql:) instead.
|
||||
""",
|
||||
sql: sql)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns a prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.cachedStatement(sql: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.cachedStatement(sql: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// The returned statement may have already been used: it may or may not
|
||||
/// contain values for its eventual arguments.
|
||||
///
|
||||
/// - parameter sql: An SQL string.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func cachedStatement(sql: String) throws -> Statement {
|
||||
try publicStatementCache.statement(sql)
|
||||
}
|
||||
|
||||
/// Returns a prepared statement that can be reused.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statement = try db.cachedStatement(literal: "SELECT * FROM player WHERE id = ?")
|
||||
/// let player1 = try Player.fetchOne(statement, arguments: [1])!
|
||||
/// let player2 = try Player.fetchOne(statement, arguments: [2])!
|
||||
///
|
||||
/// let statement = try db.cachedStatement(literal: "INSERT INTO player (name) VALUES (?)")
|
||||
/// try statement.execute(arguments: ["Arthur"])
|
||||
/// try statement.execute(arguments: ["Barbara"])
|
||||
/// ```
|
||||
///
|
||||
/// In the provided literal, no argument must be set, or all arguments must
|
||||
/// be set. An error is raised otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try cachedStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?
|
||||
/// """)
|
||||
/// try cachedStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score > \(1000)
|
||||
/// """)
|
||||
///
|
||||
/// // NOT OK (first argument is not set, but second is)
|
||||
/// try cachedStatement(literal: """
|
||||
/// SELECT COUNT(*) FROM player
|
||||
/// WHERE color = ? AND score > \(1000)
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - returns: A prepared statement.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func cachedStatement(literal sqlLiteral: SQL) throws -> Statement {
|
||||
let (sql, arguments) = try sqlLiteral.build(self)
|
||||
let statement = try cachedStatement(sql: sql)
|
||||
if arguments.isEmpty == false {
|
||||
// Throws if arguments do not match
|
||||
try statement.setArguments(arguments)
|
||||
}
|
||||
return statement
|
||||
}
|
||||
|
||||
/// Returns a cached statement that does not conflict with user's cached statements.
|
||||
func internalCachedStatement(sql: String) throws -> Statement {
|
||||
try internalStatementCache.statement(sql)
|
||||
}
|
||||
|
||||
/// Returns a cursor of prepared statements.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statements = try db.allStatements(sql: """
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
|
||||
/// while let statement = try statements.next() {
|
||||
/// try statement.execute()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The `arguments` parameter must be nil, or all arguments must be set. The
|
||||
/// returned cursor will throw an error otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try allStatements(sql: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """)
|
||||
///
|
||||
/// try allStatements(sql: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """, arguments: [1000, 1000])
|
||||
///
|
||||
/// // NOT OK (first argument is set, but second is not)
|
||||
/// try allStatements(sql: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """, arguments: [1000])
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - returns: A cursor of prepared ``Statement``.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func allStatements(sql: String, arguments: StatementArguments? = nil)
|
||||
throws -> SQLStatementCursor
|
||||
{
|
||||
SQLStatementCursor(database: self, sql: sql, arguments: arguments)
|
||||
}
|
||||
|
||||
/// Returns a cursor of prepared statements.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// let statements = try db.allStatements(literal: """
|
||||
/// INSERT INTO player (name) VALUES (\("Arthur"));
|
||||
/// INSERT INTO player (name) VALUES (\("Barbara"));
|
||||
/// INSERT INTO player (name) VALUES (\("O'Brien"));
|
||||
/// """)
|
||||
/// while let statement = try statements.next() {
|
||||
/// try statement.execute()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// In the provided literal, no argument must be set, or all arguments must
|
||||
/// be set. The returned cursor will throw an error otherwise. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // OK
|
||||
/// try allStatements(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < ?;
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """)
|
||||
///
|
||||
/// try allStatements(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < \(1000);
|
||||
/// SELECT COUNT(*) FROM player WHERE score > \(1000);
|
||||
/// """)
|
||||
///
|
||||
/// // NOT OK (first argument is set, but second is not)
|
||||
/// try allStatements(literal: """
|
||||
/// SELECT COUNT(*) FROM player WHERE score < \(1000);
|
||||
/// SELECT COUNT(*) FROM player WHERE score > ?;
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - returns: A cursor of prepared ``Statement``.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func allStatements(literal sqlLiteral: SQL) throws -> SQLStatementCursor {
|
||||
let context = SQLGenerationContext(self)
|
||||
let sql = try sqlLiteral.sql(context)
|
||||
let arguments = context.arguments.isEmpty
|
||||
? nil // builds statements without arguments
|
||||
: context.arguments // force arguments to match
|
||||
return SQLStatementCursor(database: self, sql: sql, arguments: arguments)
|
||||
}
|
||||
|
||||
/// Executes one or several SQL statements.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.execute(
|
||||
/// sql: "INSERT INTO player (name) VALUES (:name)",
|
||||
/// arguments: ["name": "Arthur"])
|
||||
///
|
||||
/// try db.execute(sql: """
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// INSERT INTO player (name) VALUES (?);
|
||||
/// """, arguments: ["Arthur", "Barbara", "O'Brien"])
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func execute(sql: String, arguments: StatementArguments = StatementArguments()) throws {
|
||||
try execute(literal: SQL(sql: sql, arguments: arguments))
|
||||
}
|
||||
|
||||
/// Executes one or several SQL statements.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.execute(literal: """
|
||||
/// INSERT INTO player (name) VALUES (\("Arthur"))
|
||||
/// """)
|
||||
///
|
||||
/// try db.execute(literal: """
|
||||
/// INSERT INTO player (name) VALUES (\("Arthur"));
|
||||
/// INSERT INTO player (name) VALUES (\("Barbara"));
|
||||
/// INSERT INTO player (name) VALUES (\("O'Brien"));
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter sqlLiteral: An ``SQL`` literal.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func execute(literal sqlLiteral: SQL) throws {
|
||||
let statements = try allStatements(literal: sqlLiteral)
|
||||
while let statement = try statements.next() {
|
||||
try statement.execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cursor over all statements in an SQL string.
|
||||
public class SQLStatementCursor {
|
||||
private let database: Database
|
||||
private let cString: ContiguousArray<CChar>
|
||||
private let prepFlags: CUnsignedInt
|
||||
private let initialArgumentCount: Int?
|
||||
|
||||
// Mutated by iteration
|
||||
private var offset: Int // offset in the C string
|
||||
private var arguments: StatementArguments? // Nil when arguments are set later
|
||||
|
||||
init(database: Database, sql: String, arguments: StatementArguments?, prepFlags: CUnsignedInt = 0) {
|
||||
self.database = database
|
||||
self.cString = sql.utf8CString
|
||||
self.prepFlags = prepFlags
|
||||
self.initialArgumentCount = arguments?.values.count
|
||||
self.offset = 0
|
||||
self.arguments = arguments
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit non-conformance to Sendable: database cursors must be used from
|
||||
// a serialized database access dispatch queue.
|
||||
@available(*, unavailable)
|
||||
extension SQLStatementCursor: Sendable { }
|
||||
|
||||
extension SQLStatementCursor: Cursor {
|
||||
public func next() throws -> Statement? {
|
||||
guard offset < cString.count - 1 /* trailing \0 */ else {
|
||||
// End of C string -> end of cursor.
|
||||
try checkArgumentsAreEmpty()
|
||||
return nil
|
||||
}
|
||||
|
||||
return try cString.withUnsafeBufferPointer { buffer in
|
||||
let baseAddress = buffer.baseAddress! // never nil because the buffer contains the trailing \0.
|
||||
|
||||
// Compile next statement
|
||||
var statementEnd: UnsafePointer<CChar>? = nil
|
||||
let statement = try Statement(
|
||||
database: database,
|
||||
statementStart: baseAddress + offset,
|
||||
statementEnd: &statementEnd,
|
||||
prepFlags: prepFlags)
|
||||
|
||||
// Advance to next statement
|
||||
offset = statementEnd! - baseAddress // never nil because statement compilation did not fail.
|
||||
|
||||
guard let statement else {
|
||||
// No statement found -> end of cursor.
|
||||
try checkArgumentsAreEmpty()
|
||||
return nil
|
||||
}
|
||||
|
||||
if arguments != nil {
|
||||
// Extract statement arguments
|
||||
let bindings = try arguments!.extractBindings(
|
||||
forStatement: statement,
|
||||
allowingRemainingValues: true)
|
||||
// unchecked is OK because we just extracted the correct
|
||||
// number of arguments
|
||||
statement.setUncheckedArguments(StatementArguments(bindings))
|
||||
}
|
||||
|
||||
return statement
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that all arguments were consumed: it is a programmer error to
|
||||
/// provide arguments that do not match the statements.
|
||||
private func checkArgumentsAreEmpty() throws {
|
||||
if let arguments,
|
||||
let initialArgumentCount,
|
||||
arguments.values.isEmpty == false
|
||||
{
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: "wrong number of statement arguments: \(initialArgumentCount)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Database {
|
||||
/// Makes sure statement can be executed, and prepares database observation.
|
||||
@usableFromInline
|
||||
func statementWillExecute(_ statement: Statement) throws {
|
||||
// Aborted transactions prevent statement execution (see the
|
||||
// documentation of this method for more information).
|
||||
try checkForAbortedTransaction(sql: statement.sql, arguments: statement.arguments)
|
||||
|
||||
// Suspended databases must not execute statements that create the risk
|
||||
// of `0xdead10cc` exception (see the documentation of this method for
|
||||
// more information).
|
||||
try checkForSuspensionViolation(from: statement)
|
||||
|
||||
// Record the database region selected by the statement execution.
|
||||
try registerAccess(to: statement.databaseRegion)
|
||||
|
||||
// Database observation: prepare transaction observers.
|
||||
observationBroker?.statementWillExecute(statement)
|
||||
}
|
||||
|
||||
/// May throw a cancelled commit error, if a transaction observer cancels
|
||||
/// an empty transaction.
|
||||
@usableFromInline
|
||||
func statementDidExecute(_ statement: Statement) throws {
|
||||
if statement.invalidatesDatabaseSchemaCache {
|
||||
clearSchemaCache()
|
||||
}
|
||||
|
||||
checkForAutocommitTransition()
|
||||
|
||||
// Database observation: cleanup
|
||||
try observationBroker?.statementDidExecute(statement)
|
||||
}
|
||||
|
||||
/// Always throws an error
|
||||
@usableFromInline
|
||||
func statementDidFail(_ statement: Statement, withResultCode resultCode: CInt) throws -> Never {
|
||||
// Failed statements can not be reused, because `sqlite3_reset` won't
|
||||
// be able to restore the statement to its initial state:
|
||||
// https://www.sqlite.org/c3ref/reset.html
|
||||
//
|
||||
// So make sure we clear this statement from the cache.
|
||||
internalStatementCache.remove(statement)
|
||||
publicStatementCache.remove(statement)
|
||||
|
||||
checkForAutocommitTransition()
|
||||
|
||||
// Extract values that may be modified by the user in their
|
||||
// `TransactionObserver.databaseDidRollback(_:)` implementation
|
||||
// (see below).
|
||||
let message = lastErrorMessage
|
||||
let arguments = statement.arguments
|
||||
|
||||
// Database observation: cleanup.
|
||||
//
|
||||
// If the statement failure is due to a transaction observer that has
|
||||
// cancelled a transaction, this calls `TransactionObserver.databaseDidRollback(_:)`,
|
||||
// and throws the user-provided cancelled commit error.
|
||||
try observationBroker?.statementDidFail(statement)
|
||||
|
||||
// Throw statement failure
|
||||
throw DatabaseError(
|
||||
resultCode: resultCode,
|
||||
message: message,
|
||||
sql: statement.sql,
|
||||
arguments: arguments,
|
||||
publicStatementArguments: configuration.publicStatementArguments)
|
||||
}
|
||||
|
||||
private func checkForAutocommitTransition() {
|
||||
if sqlite3_get_autocommit(sqliteConnection) == 0 {
|
||||
if autocommitState == .on {
|
||||
// Record transaction date as soon as the connection leaves
|
||||
// auto-commit mode.
|
||||
// We grab a result, so that this failure is later reported
|
||||
// whenever the user calls `Database.transactionDate`.
|
||||
transactionDateResult = Result { try configuration.transactionClock.now(self) }
|
||||
}
|
||||
autocommitState = .off
|
||||
} else {
|
||||
if autocommitState == .off {
|
||||
// Reset transaction date
|
||||
transactionDateResult = nil
|
||||
}
|
||||
autocommitState = .on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread-unsafe statement cache
|
||||
struct StatementCache {
|
||||
unowned let db: Database
|
||||
private var statements: [String: Statement] = [:]
|
||||
|
||||
init(database: Database) {
|
||||
self.db = database
|
||||
}
|
||||
|
||||
mutating func statement(_ sql: String) throws -> Statement {
|
||||
if let statement = statements[sql] {
|
||||
return statement
|
||||
}
|
||||
|
||||
// http://www.sqlite.org/c3ref/c_prepare_persistent.html#sqlitepreparepersistent
|
||||
// > The SQLITE_PREPARE_PERSISTENT flag is a hint to the query
|
||||
// > planner that the prepared statement will be retained for a long
|
||||
// > time and probably reused many times.
|
||||
//
|
||||
// This looks like a perfect match for cached statements.
|
||||
//
|
||||
// However SQLITE_PREPARE_PERSISTENT was only introduced in
|
||||
// SQLite 3.20.0 http://www.sqlite.org/changes.html#version_3_20
|
||||
#if GRDBCUSTOMSQLITE || GRDBCIPHER
|
||||
let statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT))
|
||||
#else
|
||||
let statement: Statement
|
||||
if #available(iOS 12, macOS 10.14, watchOS 5, *) { // SQLite 3.20+
|
||||
statement = try db.makeStatement(sql: sql, prepFlags: CUnsignedInt(SQLITE_PREPARE_PERSISTENT))
|
||||
} else {
|
||||
statement = try db.makeStatement(sql: sql)
|
||||
}
|
||||
#endif
|
||||
statements[sql] = statement
|
||||
return statement
|
||||
}
|
||||
|
||||
mutating func clear() {
|
||||
statements = [:]
|
||||
}
|
||||
|
||||
mutating func remove(_ statement: Statement) {
|
||||
statements.removeFirst { $0.value === statement }
|
||||
}
|
||||
|
||||
mutating func removeAll(where shouldBeRemoved: (Statement) -> Bool) {
|
||||
statements = statements.filter { (_, statement) in !shouldBeRemoved(statement) }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
/// Describe the progress of a database backup.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/backup_finish.html>
|
||||
public struct DatabaseBackupProgress: Sendable {
|
||||
/// The number of pages still to be backed up.
|
||||
///
|
||||
/// It is the result of the `sqlite3_backup_remaining` function.
|
||||
public let remainingPageCount: Int
|
||||
|
||||
/// The number of pages in the source database.
|
||||
///
|
||||
/// It is the result of the `sqlite3_backup_pagecount` function.
|
||||
public let totalPageCount: Int
|
||||
|
||||
/// The number of of backed up pages.
|
||||
///
|
||||
/// It is equal to `totalPageCount - remainingPageCount`.
|
||||
public var completedPageCount: Int {
|
||||
totalPageCount - remainingPageCount
|
||||
}
|
||||
|
||||
/// A boolean value indicating whether the backup is complete.
|
||||
///
|
||||
/// It is true if and only if the last call the `sqlite3_backup_step` has
|
||||
/// returned `SQLITE_DONE`.
|
||||
public let isCompleted: Bool
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
|
||||
/// `DatabaseCollation` is a custom string comparison function used by SQLite.
|
||||
///
|
||||
/// See also ``Database/CollationName``.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/datatype3.html#collating_sequences>
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating a Custom Collation
|
||||
///
|
||||
/// - ``init(_:function:)``
|
||||
/// - ``name``
|
||||
///
|
||||
/// ### Built-in Collations
|
||||
///
|
||||
/// - ``caseInsensitiveCompare``
|
||||
/// - ``localizedCaseInsensitiveCompare``
|
||||
/// - ``localizedCompare``
|
||||
/// - ``localizedStandardCompare``
|
||||
/// - ``unicodeCompare``
|
||||
public final class DatabaseCollation {
|
||||
/// The name of the collation.
|
||||
public let name: String
|
||||
let function: (CInt, UnsafeRawPointer?, CInt, UnsafeRawPointer?) -> ComparisonResult
|
||||
|
||||
/// Creates a collation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let collation = DatabaseCollation("localized_standard") { (string1, string2) in
|
||||
/// return (string1 as NSString).localizedStandardCompare(string2)
|
||||
/// }
|
||||
/// db.add(collation: collation)
|
||||
/// try db.execute(sql: "CREATE TABLE file (name TEXT COLLATE localized_standard")
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - name: The collation name.
|
||||
/// - function: A function that compares two strings.
|
||||
public init(_ name: String, function: @escaping (String, String) -> ComparisonResult) {
|
||||
self.name = name
|
||||
self.function = { (length1, buffer1, length2, buffer2) in
|
||||
// Buffers are not C strings: they do not end with \0.
|
||||
let string1 = String(
|
||||
bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer1.unsafelyUnwrapped),
|
||||
length: Int(length1),
|
||||
encoding: .utf8,
|
||||
freeWhenDone: false)!
|
||||
let string2 = String(
|
||||
bytesNoCopy: UnsafeMutableRawPointer(mutating: buffer2.unsafelyUnwrapped),
|
||||
length: Int(length2),
|
||||
encoding: .utf8,
|
||||
freeWhenDone: false)!
|
||||
return function(string1, string2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseCollation: Hashable {
|
||||
// Collation equality is based on the sqlite3_strnicmp SQLite function.
|
||||
// (see https://www.sqlite.org/c3ref/create_collation.html). Computing
|
||||
// a hash value that honors the Swift Hashable contract (value equality
|
||||
// implies hash equality) is thus non trivial. But it's not that
|
||||
// important, since this hashValue is only used when one adds
|
||||
// or removes a collation from a database connection.
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(0)
|
||||
}
|
||||
|
||||
/// Two collations are equal if they share the same name (case insensitive)
|
||||
public static func == (lhs: DatabaseCollation, rhs: DatabaseCollation) -> Bool {
|
||||
// See <https://www.sqlite.org/c3ref/create_collation.html>
|
||||
return sqlite3_stricmp(lhs.name, rhs.name) == 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
import Foundation
|
||||
|
||||
/// An SQLite result code.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
|
||||
public struct ResultCode: RawRepresentable, Equatable {
|
||||
/// The raw SQLite result code.
|
||||
public let rawValue: CInt
|
||||
|
||||
/// Creates a `ResultCode` from a raw SQLite result code.
|
||||
public init(rawValue: CInt) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
/// A primary result code limited to the least significant 8 bits.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let resultCode = .SQLITE_CONSTRAINT_FOREIGNKEY
|
||||
/// resultCode.primaryResultCode == .SQLITE_CONSTRAINT // true
|
||||
/// ```
|
||||
public var primaryResultCode: ResultCode {
|
||||
ResultCode(rawValue: rawValue & 0xFF)
|
||||
}
|
||||
|
||||
var isPrimary: Bool { self == primaryResultCode }
|
||||
|
||||
/// Returns true if the code on the left matches the code on the right.
|
||||
///
|
||||
/// Primary result codes match themselves and their extended result codes,
|
||||
/// while extended result codes match only themselves:
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// switch error.extendedResultCode {
|
||||
/// case .SQLITE_CONSTRAINT_FOREIGNKEY: // foreign key constraint error
|
||||
/// case .SQLITE_CONSTRAINT: // any other constraint error
|
||||
/// default: // any other database error
|
||||
/// }
|
||||
/// ```
|
||||
public static func ~= (pattern: ResultCode, code: ResultCode) -> Bool {
|
||||
if pattern.isPrimary {
|
||||
return pattern == code.primaryResultCode
|
||||
} else {
|
||||
return pattern == code
|
||||
}
|
||||
}
|
||||
|
||||
// Primary Result codes
|
||||
// https://www.sqlite.org/rescode.html#primary_result_code_list
|
||||
|
||||
// swiftlint:disable operator_usage_whitespace
|
||||
public static let SQLITE_OK = ResultCode(rawValue: 0) // Successful result
|
||||
public static let SQLITE_ERROR = ResultCode(rawValue: 1) // SQL error or missing database
|
||||
public static let SQLITE_INTERNAL = ResultCode(rawValue: 2) // Internal logic error in SQLite
|
||||
public static let SQLITE_PERM = ResultCode(rawValue: 3) // Access permission denied
|
||||
public static let SQLITE_ABORT = ResultCode(rawValue: 4) // Callback routine requested an abort
|
||||
public static let SQLITE_BUSY = ResultCode(rawValue: 5) // The database file is locked
|
||||
public static let SQLITE_LOCKED = ResultCode(rawValue: 6) // A table in the database is locked
|
||||
public static let SQLITE_NOMEM = ResultCode(rawValue: 7) // A malloc() failed
|
||||
public static let SQLITE_READONLY = ResultCode(rawValue: 8) // Attempt to write a readonly database
|
||||
public static let SQLITE_INTERRUPT = ResultCode(rawValue: 9) // Operation terminated by sqlite3_interrupt()
|
||||
public static let SQLITE_IOERR = ResultCode(rawValue: 10) // Some kind of disk I/O error occurred
|
||||
public static let SQLITE_CORRUPT = ResultCode(rawValue: 11) // The database disk image is malformed
|
||||
public static let SQLITE_NOTFOUND = ResultCode(rawValue: 12) // Unknown opcode in sqlite3_file_control()
|
||||
public static let SQLITE_FULL = ResultCode(rawValue: 13) // Insertion failed because database is full
|
||||
public static let SQLITE_CANTOPEN = ResultCode(rawValue: 14) // Unable to open the database file
|
||||
public static let SQLITE_PROTOCOL = ResultCode(rawValue: 15) // Database lock protocol error
|
||||
public static let SQLITE_EMPTY = ResultCode(rawValue: 16) // Database is empty
|
||||
public static let SQLITE_SCHEMA = ResultCode(rawValue: 17) // The database schema changed
|
||||
public static let SQLITE_TOOBIG = ResultCode(rawValue: 18) // String or BLOB exceeds size limit
|
||||
public static let SQLITE_CONSTRAINT = ResultCode(rawValue: 19) // Abort due to constraint violation
|
||||
public static let SQLITE_MISMATCH = ResultCode(rawValue: 20) // Data type mismatch
|
||||
public static let SQLITE_MISUSE = ResultCode(rawValue: 21) // Library used incorrectly
|
||||
public static let SQLITE_NOLFS = ResultCode(rawValue: 22) // Uses OS features not supported on host
|
||||
public static let SQLITE_AUTH = ResultCode(rawValue: 23) // Authorization denied
|
||||
public static let SQLITE_FORMAT = ResultCode(rawValue: 24) // Auxiliary database format error
|
||||
public static let SQLITE_RANGE = ResultCode(rawValue: 25) // 2nd parameter to sqlite3_bind out of range
|
||||
public static let SQLITE_NOTADB = ResultCode(rawValue: 26) // File opened that is not a database file
|
||||
public static let SQLITE_NOTICE = ResultCode(rawValue: 27) // Notifications from sqlite3_log()
|
||||
public static let SQLITE_WARNING = ResultCode(rawValue: 28) // Warnings from sqlite3_log()
|
||||
public static let SQLITE_ROW = ResultCode(rawValue: 100) // sqlite3_step() has another row ready
|
||||
public static let SQLITE_DONE = ResultCode(rawValue: 101) // sqlite3_step() has finished executing
|
||||
// swiftlint:enable operator_usage_whitespace
|
||||
|
||||
// Extended Result Code
|
||||
// https://www.sqlite.org/rescode.html#extended_result_code_list
|
||||
|
||||
// swiftlint:disable operator_usage_whitespace line_length
|
||||
public static let SQLITE_ERROR_MISSING_COLLSEQ = ResultCode(rawValue: (SQLITE_ERROR.rawValue | (1<<8)))
|
||||
public static let SQLITE_ERROR_RETRY = ResultCode(rawValue: (SQLITE_ERROR.rawValue | (2<<8)))
|
||||
public static let SQLITE_ERROR_SNAPSHOT = ResultCode(rawValue: (SQLITE_ERROR.rawValue | (3<<8)))
|
||||
public static let SQLITE_IOERR_READ = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (1<<8)))
|
||||
public static let SQLITE_IOERR_SHORT_READ = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (2<<8)))
|
||||
public static let SQLITE_IOERR_WRITE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (3<<8)))
|
||||
public static let SQLITE_IOERR_FSYNC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (4<<8)))
|
||||
public static let SQLITE_IOERR_DIR_FSYNC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (5<<8)))
|
||||
public static let SQLITE_IOERR_TRUNCATE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (6<<8)))
|
||||
public static let SQLITE_IOERR_FSTAT = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (7<<8)))
|
||||
public static let SQLITE_IOERR_UNLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (8<<8)))
|
||||
public static let SQLITE_IOERR_RDLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (9<<8)))
|
||||
public static let SQLITE_IOERR_DELETE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (10<<8)))
|
||||
public static let SQLITE_IOERR_BLOCKED = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (11<<8)))
|
||||
public static let SQLITE_IOERR_NOMEM = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (12<<8)))
|
||||
public static let SQLITE_IOERR_ACCESS = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (13<<8)))
|
||||
public static let SQLITE_IOERR_CHECKRESERVEDLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (14<<8)))
|
||||
public static let SQLITE_IOERR_LOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (15<<8)))
|
||||
public static let SQLITE_IOERR_CLOSE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (16<<8)))
|
||||
public static let SQLITE_IOERR_DIR_CLOSE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (17<<8)))
|
||||
public static let SQLITE_IOERR_SHMOPEN = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (18<<8)))
|
||||
public static let SQLITE_IOERR_SHMSIZE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (19<<8)))
|
||||
public static let SQLITE_IOERR_SHMLOCK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (20<<8)))
|
||||
public static let SQLITE_IOERR_SHMMAP = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (21<<8)))
|
||||
public static let SQLITE_IOERR_SEEK = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (22<<8)))
|
||||
public static let SQLITE_IOERR_DELETE_NOENT = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (23<<8)))
|
||||
public static let SQLITE_IOERR_MMAP = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (24<<8)))
|
||||
public static let SQLITE_IOERR_GETTEMPPATH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (25<<8)))
|
||||
public static let SQLITE_IOERR_CONVPATH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (26<<8)))
|
||||
public static let SQLITE_IOERR_VNODE = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (27<<8)))
|
||||
public static let SQLITE_IOERR_AUTH = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (28<<8)))
|
||||
public static let SQLITE_IOERR_BEGIN_ATOMIC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (29<<8)))
|
||||
public static let SQLITE_IOERR_COMMIT_ATOMIC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (30<<8)))
|
||||
public static let SQLITE_IOERR_ROLLBACK_ATOMIC = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (31<<8)))
|
||||
public static let SQLITE_IOERR_DATA = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (32<<8)))
|
||||
public static let SQLITE_IOERR_CORRUPTFS = ResultCode(rawValue: (SQLITE_IOERR.rawValue | (33<<8)))
|
||||
public static let SQLITE_LOCKED_SHAREDCACHE = ResultCode(rawValue: (SQLITE_LOCKED.rawValue | (1<<8)))
|
||||
public static let SQLITE_LOCKED_VTAB = ResultCode(rawValue: (SQLITE_LOCKED.rawValue | (2<<8)))
|
||||
public static let SQLITE_BUSY_RECOVERY = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (1<<8)))
|
||||
public static let SQLITE_BUSY_SNAPSHOT = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (2<<8)))
|
||||
public static let SQLITE_BUSY_TIMEOUT = ResultCode(rawValue: (SQLITE_BUSY.rawValue | (3<<8)))
|
||||
public static let SQLITE_CANTOPEN_NOTEMPDIR = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (1<<8)))
|
||||
public static let SQLITE_CANTOPEN_ISDIR = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (2<<8)))
|
||||
public static let SQLITE_CANTOPEN_FULLPATH = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (3<<8)))
|
||||
public static let SQLITE_CANTOPEN_CONVPATH = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (4<<8)))
|
||||
public static let SQLITE_CANTOPEN_DIRTYWAL = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (5<<8))) /* Not Used */
|
||||
public static let SQLITE_CANTOPEN_SYMLINK = ResultCode(rawValue: (SQLITE_CANTOPEN.rawValue | (6<<8)))
|
||||
public static let SQLITE_CORRUPT_VTAB = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (1<<8)))
|
||||
public static let SQLITE_CORRUPT_SEQUENCE = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (2<<8)))
|
||||
public static let SQLITE_CORRUPT_INDEX = ResultCode(rawValue: (SQLITE_CORRUPT.rawValue | (3<<8)))
|
||||
public static let SQLITE_READONLY_RECOVERY = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (1<<8)))
|
||||
public static let SQLITE_READONLY_CANTLOCK = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (2<<8)))
|
||||
public static let SQLITE_READONLY_ROLLBACK = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (3<<8)))
|
||||
public static let SQLITE_READONLY_DBMOVED = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (4<<8)))
|
||||
public static let SQLITE_READONLY_CANTINIT = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (5<<8)))
|
||||
public static let SQLITE_READONLY_DIRECTORY = ResultCode(rawValue: (SQLITE_READONLY.rawValue | (6<<8)))
|
||||
public static let SQLITE_ABORT_ROLLBACK = ResultCode(rawValue: (SQLITE_ABORT.rawValue | (2<<8)))
|
||||
public static let SQLITE_CONSTRAINT_CHECK = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (1<<8)))
|
||||
public static let SQLITE_CONSTRAINT_COMMITHOOK = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (2<<8)))
|
||||
public static let SQLITE_CONSTRAINT_FOREIGNKEY = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (3<<8)))
|
||||
public static let SQLITE_CONSTRAINT_FUNCTION = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (4<<8)))
|
||||
public static let SQLITE_CONSTRAINT_NOTNULL = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (5<<8)))
|
||||
public static let SQLITE_CONSTRAINT_PRIMARYKEY = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (6<<8)))
|
||||
public static let SQLITE_CONSTRAINT_TRIGGER = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (7<<8)))
|
||||
public static let SQLITE_CONSTRAINT_UNIQUE = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (8<<8)))
|
||||
public static let SQLITE_CONSTRAINT_VTAB = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (9<<8)))
|
||||
public static let SQLITE_CONSTRAINT_ROWID = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (10<<8)))
|
||||
public static let SQLITE_CONSTRAINT_PINNED = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (11<<8)))
|
||||
public static let SQLITE_CONSTRAINT_DATATYPE = ResultCode(rawValue: (SQLITE_CONSTRAINT.rawValue | (12<<8)))
|
||||
public static let SQLITE_NOTICE_RECOVER_WAL = ResultCode(rawValue: (SQLITE_NOTICE.rawValue | (1<<8)))
|
||||
public static let SQLITE_NOTICE_RECOVER_ROLLBACK = ResultCode(rawValue: (SQLITE_NOTICE.rawValue | (2<<8)))
|
||||
public static let SQLITE_WARNING_AUTOINDEX = ResultCode(rawValue: (SQLITE_WARNING.rawValue | (1<<8)))
|
||||
public static let SQLITE_AUTH_USER = ResultCode(rawValue: (SQLITE_AUTH.rawValue | (1<<8)))
|
||||
public static let SQLITE_OK_LOAD_PERMANENTLY = ResultCode(rawValue: (SQLITE_OK.rawValue | (1<<8)))
|
||||
public static let SQLITE_OK_SYMLINK = ResultCode(rawValue: (SQLITE_OK.rawValue | (2<<8)))
|
||||
// swiftlint:enable operator_usage_whitespace line_length
|
||||
}
|
||||
|
||||
extension ResultCode {
|
||||
/// Returns true if the code on the left matches the error on the right.
|
||||
///
|
||||
/// Primary result codes match themselves and their extended result codes,
|
||||
/// while extended result codes match only themselves.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// try ...
|
||||
/// } catch ResultCode.SQLITE_CONSTRAINT_FOREIGNKEY {
|
||||
/// // foreign key constraint error
|
||||
/// } catch ResultCode.SQLITE_CONSTRAINT {
|
||||
/// // any other constraint error
|
||||
/// } catch {
|
||||
/// // any other database error
|
||||
/// }
|
||||
/// ```
|
||||
public static func ~= (lhs: Self, rhs: Error) -> Bool {
|
||||
guard let error = rhs as? DatabaseError else { return false }
|
||||
return lhs ~= error.extendedResultCode
|
||||
}
|
||||
}
|
||||
|
||||
extension ResultCode: CustomStringConvertible {
|
||||
var errorString: String? {
|
||||
String(cString: sqlite3_errstr(rawValue))
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
if let errorString {
|
||||
return "\(rawValue) (\(errorString))"
|
||||
} else {
|
||||
return "\(rawValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ResultCode: Sendable { }
|
||||
|
||||
/// A `DatabaseError` describes an SQLite error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// try player.insert(db)
|
||||
/// } catch let error as DatabaseError {
|
||||
/// print(error) // prints debugging information
|
||||
///
|
||||
/// switch error {
|
||||
/// case DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY:
|
||||
/// // foreign key constraint error
|
||||
/// case DatabaseError.SQLITE_CONSTRAINT:
|
||||
/// // any other constraint error
|
||||
/// default:
|
||||
/// // any other database error
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The above example can also be written in a shorter way:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// try player.insert(db)
|
||||
/// } catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY {
|
||||
/// // foreign key constraint error
|
||||
/// } catch DatabaseError.SQLITE_CONSTRAINT {
|
||||
/// // any other constraint error
|
||||
/// } catch {
|
||||
/// // any other database error
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating DatabaseError
|
||||
///
|
||||
/// - ``init(resultCode:message:sql:arguments:publicStatementArguments:)``
|
||||
/// - ``ResultCode``
|
||||
///
|
||||
/// ### Error Information
|
||||
///
|
||||
/// - ``arguments``
|
||||
/// - ``extendedResultCode``
|
||||
/// - ``isInterruptionError``
|
||||
/// - ``message``
|
||||
/// - ``resultCode``
|
||||
/// - ``sql``
|
||||
///
|
||||
/// ### Converting DatabaseError to String
|
||||
///
|
||||
/// - ``description``
|
||||
/// - ``expandedDescription``
|
||||
public struct DatabaseError: Error {
|
||||
/// The SQLite primary result code.
|
||||
///
|
||||
/// This property returns a "primary result code", that is to say the least
|
||||
/// significant 8 bits of any SQLite result code.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// ...
|
||||
/// } catch let error as DatabaseError where error.resultCode == .SQL_CONSTRAINT {
|
||||
/// // A constraint error
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The above example can also be written in a shorter way:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// ...
|
||||
/// } catch DatabaseError.SQL_CONSTRAINT {
|
||||
/// // A constraint error
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also ``extendedResultCode``.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
|
||||
public var resultCode: ResultCode {
|
||||
extendedResultCode.primaryResultCode
|
||||
}
|
||||
|
||||
/// The SQLite extended error code.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// ...
|
||||
/// } catch let error as DatabaseError where error.extendedResultCode == .SQLITE_CONSTRAINT_FOREIGNKEY {
|
||||
/// // A foreign key constraint error
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The above example can also be written in a shorter way:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// ...
|
||||
/// } catch DatabaseError.SQLITE_CONSTRAINT_FOREIGNKEY {
|
||||
/// // A foreign key constraint error
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// See also ``resultCode``.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/rescode.html>
|
||||
public let extendedResultCode: ResultCode
|
||||
|
||||
/// The SQLite error message.
|
||||
public let message: String?
|
||||
|
||||
/// The SQL query that yielded the error.
|
||||
public let sql: String?
|
||||
|
||||
/// The query arguments that yielded the error.
|
||||
public let arguments: StatementArguments?
|
||||
|
||||
/// See Configuration.publicStatementArguments
|
||||
var publicStatementArguments: Bool
|
||||
|
||||
/// Creates a `DatabaseError`.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - resultCode: A ResultCode (defaults to .SQLITE_ERROR).
|
||||
/// - message: An eventual error message. If nil, the error message is
|
||||
/// derived from the result code.
|
||||
/// - sql: An eventual SQL string.
|
||||
/// - arguments: Eventual statement arguments.
|
||||
/// - publicStatementArguments: If false (the default), statement
|
||||
/// arguments are not visible in the error's ``description`` property.
|
||||
public init(
|
||||
resultCode: ResultCode = .SQLITE_ERROR,
|
||||
message: String? = nil,
|
||||
sql: String? = nil,
|
||||
arguments: StatementArguments? = nil,
|
||||
publicStatementArguments: Bool = false)
|
||||
{
|
||||
self.extendedResultCode = resultCode
|
||||
self.message = message ?? resultCode.errorString
|
||||
self.sql = sql
|
||||
self.arguments = arguments
|
||||
self.publicStatementArguments = publicStatementArguments
|
||||
}
|
||||
|
||||
/// Creates a Database Error with a raw CInt result code.
|
||||
///
|
||||
/// This initializer is not public because library user is not supposed to
|
||||
/// be exposed to raw result codes.
|
||||
@usableFromInline
|
||||
init(resultCode: CInt, message: String? = nil, sql: String? = nil) {
|
||||
self.init(
|
||||
resultCode: ResultCode(rawValue: resultCode),
|
||||
message: message,
|
||||
sql: sql)
|
||||
}
|
||||
|
||||
/// Creates a Database Error with a raw CInt result code.
|
||||
///
|
||||
/// This initializer is not public because library user is not supposed to
|
||||
/// be exposed to raw result codes.
|
||||
@usableFromInline
|
||||
init(
|
||||
resultCode: CInt,
|
||||
message: String? = nil,
|
||||
sql: String? = nil,
|
||||
arguments: StatementArguments?,
|
||||
publicStatementArguments: Bool)
|
||||
{
|
||||
self.init(
|
||||
resultCode: ResultCode(rawValue: resultCode),
|
||||
message: message,
|
||||
sql: sql,
|
||||
arguments: arguments,
|
||||
publicStatementArguments: publicStatementArguments)
|
||||
}
|
||||
|
||||
static func noSuchTable(_ tableName: String) -> Self {
|
||||
DatabaseError(message: "no such table: \(tableName)")
|
||||
}
|
||||
|
||||
static func noSuchSchema(_ schemaName: String) -> Self {
|
||||
DatabaseError(message: "no such schema: \(schemaName)")
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseError {
|
||||
static func connectionIsClosed() -> Self {
|
||||
DatabaseError(resultCode: .SQLITE_MISUSE, message: "Connection is closed")
|
||||
}
|
||||
}
|
||||
|
||||
// Support for `catch DatabaseError.SQLITE_XXX`
|
||||
extension DatabaseError {
|
||||
public static let SQLITE_OK = ResultCode.SQLITE_OK
|
||||
public static let SQLITE_ERROR = ResultCode.SQLITE_ERROR
|
||||
public static let SQLITE_INTERNAL = ResultCode.SQLITE_INTERNAL
|
||||
public static let SQLITE_PERM = ResultCode.SQLITE_PERM
|
||||
public static let SQLITE_ABORT = ResultCode.SQLITE_ABORT
|
||||
public static let SQLITE_BUSY = ResultCode.SQLITE_BUSY
|
||||
public static let SQLITE_LOCKED = ResultCode.SQLITE_LOCKED
|
||||
public static let SQLITE_NOMEM = ResultCode.SQLITE_NOMEM
|
||||
public static let SQLITE_READONLY = ResultCode.SQLITE_READONLY
|
||||
public static let SQLITE_INTERRUPT = ResultCode.SQLITE_INTERRUPT
|
||||
public static let SQLITE_IOERR = ResultCode.SQLITE_IOERR
|
||||
public static let SQLITE_CORRUPT = ResultCode.SQLITE_CORRUPT
|
||||
public static let SQLITE_NOTFOUND = ResultCode.SQLITE_NOTFOUND
|
||||
public static let SQLITE_FULL = ResultCode.SQLITE_FULL
|
||||
public static let SQLITE_CANTOPEN = ResultCode.SQLITE_CANTOPEN
|
||||
public static let SQLITE_PROTOCOL = ResultCode.SQLITE_PROTOCOL
|
||||
public static let SQLITE_EMPTY = ResultCode.SQLITE_EMPTY
|
||||
public static let SQLITE_SCHEMA = ResultCode.SQLITE_SCHEMA
|
||||
public static let SQLITE_TOOBIG = ResultCode.SQLITE_TOOBIG
|
||||
public static let SQLITE_CONSTRAINT = ResultCode.SQLITE_CONSTRAINT
|
||||
public static let SQLITE_MISMATCH = ResultCode.SQLITE_MISMATCH
|
||||
public static let SQLITE_MISUSE = ResultCode.SQLITE_MISUSE
|
||||
public static let SQLITE_NOLFS = ResultCode.SQLITE_NOLFS
|
||||
public static let SQLITE_AUTH = ResultCode.SQLITE_AUTH
|
||||
public static let SQLITE_FORMAT = ResultCode.SQLITE_FORMAT
|
||||
public static let SQLITE_RANGE = ResultCode.SQLITE_RANGE
|
||||
public static let SQLITE_NOTADB = ResultCode.SQLITE_NOTADB
|
||||
public static let SQLITE_NOTICE = ResultCode.SQLITE_NOTICE
|
||||
public static let SQLITE_WARNING = ResultCode.SQLITE_WARNING
|
||||
public static let SQLITE_ROW = ResultCode.SQLITE_ROW
|
||||
public static let SQLITE_DONE = ResultCode.SQLITE_DONE
|
||||
public static let SQLITE_ERROR_MISSING_COLLSEQ = ResultCode.SQLITE_ERROR_MISSING_COLLSEQ
|
||||
public static let SQLITE_ERROR_RETRY = ResultCode.SQLITE_ERROR_RETRY
|
||||
public static let SQLITE_ERROR_SNAPSHOT = ResultCode.SQLITE_ERROR_SNAPSHOT
|
||||
public static let SQLITE_IOERR_READ = ResultCode.SQLITE_IOERR_READ
|
||||
public static let SQLITE_IOERR_SHORT_READ = ResultCode.SQLITE_IOERR_SHORT_READ
|
||||
public static let SQLITE_IOERR_WRITE = ResultCode.SQLITE_IOERR_WRITE
|
||||
public static let SQLITE_IOERR_FSYNC = ResultCode.SQLITE_IOERR_FSYNC
|
||||
public static let SQLITE_IOERR_DIR_FSYNC = ResultCode.SQLITE_IOERR_DIR_FSYNC
|
||||
public static let SQLITE_IOERR_TRUNCATE = ResultCode.SQLITE_IOERR_TRUNCATE
|
||||
public static let SQLITE_IOERR_FSTAT = ResultCode.SQLITE_IOERR_FSTAT
|
||||
public static let SQLITE_IOERR_UNLOCK = ResultCode.SQLITE_IOERR_UNLOCK
|
||||
public static let SQLITE_IOERR_RDLOCK = ResultCode.SQLITE_IOERR_RDLOCK
|
||||
public static let SQLITE_IOERR_DELETE = ResultCode.SQLITE_IOERR_DELETE
|
||||
public static let SQLITE_IOERR_BLOCKED = ResultCode.SQLITE_IOERR_BLOCKED
|
||||
public static let SQLITE_IOERR_NOMEM = ResultCode.SQLITE_IOERR_NOMEM
|
||||
public static let SQLITE_IOERR_ACCESS = ResultCode.SQLITE_IOERR_ACCESS
|
||||
public static let SQLITE_IOERR_CHECKRESERVEDLOCK = ResultCode.SQLITE_IOERR_CHECKRESERVEDLOCK
|
||||
public static let SQLITE_IOERR_LOCK = ResultCode.SQLITE_IOERR_LOCK
|
||||
public static let SQLITE_IOERR_CLOSE = ResultCode.SQLITE_IOERR_CLOSE
|
||||
public static let SQLITE_IOERR_DIR_CLOSE = ResultCode.SQLITE_IOERR_DIR_CLOSE
|
||||
public static let SQLITE_IOERR_SHMOPEN = ResultCode.SQLITE_IOERR_SHMOPEN
|
||||
public static let SQLITE_IOERR_SHMSIZE = ResultCode.SQLITE_IOERR_SHMSIZE
|
||||
public static let SQLITE_IOERR_SHMLOCK = ResultCode.SQLITE_IOERR_SHMLOCK
|
||||
public static let SQLITE_IOERR_SHMMAP = ResultCode.SQLITE_IOERR_SHMMAP
|
||||
public static let SQLITE_IOERR_SEEK = ResultCode.SQLITE_IOERR_SEEK
|
||||
public static let SQLITE_IOERR_DELETE_NOENT = ResultCode.SQLITE_IOERR_DELETE_NOENT
|
||||
public static let SQLITE_IOERR_MMAP = ResultCode.SQLITE_IOERR_MMAP
|
||||
public static let SQLITE_IOERR_GETTEMPPATH = ResultCode.SQLITE_IOERR_GETTEMPPATH
|
||||
public static let SQLITE_IOERR_CONVPATH = ResultCode.SQLITE_IOERR_CONVPATH
|
||||
public static let SQLITE_IOERR_VNODE = ResultCode.SQLITE_IOERR_VNODE
|
||||
public static let SQLITE_IOERR_AUTH = ResultCode.SQLITE_IOERR_AUTH
|
||||
public static let SQLITE_IOERR_BEGIN_ATOMIC = ResultCode.SQLITE_IOERR_BEGIN_ATOMIC
|
||||
public static let SQLITE_IOERR_COMMIT_ATOMIC = ResultCode.SQLITE_IOERR_COMMIT_ATOMIC
|
||||
public static let SQLITE_IOERR_ROLLBACK_ATOMIC = ResultCode.SQLITE_IOERR_ROLLBACK_ATOMIC
|
||||
public static let SQLITE_IOERR_DATA = ResultCode.SQLITE_IOERR_DATA
|
||||
public static let SQLITE_IOERR_CORRUPTFS = ResultCode.SQLITE_IOERR_CORRUPTFS
|
||||
public static let SQLITE_LOCKED_SHAREDCACHE = ResultCode.SQLITE_LOCKED_SHAREDCACHE
|
||||
public static let SQLITE_LOCKED_VTAB = ResultCode.SQLITE_LOCKED_VTAB
|
||||
public static let SQLITE_BUSY_RECOVERY = ResultCode.SQLITE_BUSY_RECOVERY
|
||||
public static let SQLITE_BUSY_SNAPSHOT = ResultCode.SQLITE_BUSY_SNAPSHOT
|
||||
public static let SQLITE_BUSY_TIMEOUT = ResultCode.SQLITE_BUSY_TIMEOUT
|
||||
public static let SQLITE_CANTOPEN_NOTEMPDIR = ResultCode.SQLITE_CANTOPEN_NOTEMPDIR
|
||||
public static let SQLITE_CANTOPEN_ISDIR = ResultCode.SQLITE_CANTOPEN_ISDIR
|
||||
public static let SQLITE_CANTOPEN_FULLPATH = ResultCode.SQLITE_CANTOPEN_FULLPATH
|
||||
public static let SQLITE_CANTOPEN_CONVPATH = ResultCode.SQLITE_CANTOPEN_CONVPATH
|
||||
public static let SQLITE_CANTOPEN_DIRTYWAL = ResultCode.SQLITE_CANTOPEN_DIRTYWAL
|
||||
public static let SQLITE_CANTOPEN_SYMLINK = ResultCode.SQLITE_CANTOPEN_SYMLINK
|
||||
public static let SQLITE_CORRUPT_VTAB = ResultCode.SQLITE_CORRUPT_VTAB
|
||||
public static let SQLITE_CORRUPT_SEQUENCE = ResultCode.SQLITE_CORRUPT_SEQUENCE
|
||||
public static let SQLITE_CORRUPT_INDEX = ResultCode.SQLITE_CORRUPT_INDEX
|
||||
public static let SQLITE_READONLY_RECOVERY = ResultCode.SQLITE_READONLY_RECOVERY
|
||||
public static let SQLITE_READONLY_CANTLOCK = ResultCode.SQLITE_READONLY_CANTLOCK
|
||||
public static let SQLITE_READONLY_ROLLBACK = ResultCode.SQLITE_READONLY_ROLLBACK
|
||||
public static let SQLITE_READONLY_DBMOVED = ResultCode.SQLITE_READONLY_DBMOVED
|
||||
public static let SQLITE_READONLY_CANTINIT = ResultCode.SQLITE_READONLY_CANTINIT
|
||||
public static let SQLITE_READONLY_DIRECTORY = ResultCode.SQLITE_READONLY_DIRECTORY
|
||||
public static let SQLITE_ABORT_ROLLBACK = ResultCode.SQLITE_ABORT_ROLLBACK
|
||||
public static let SQLITE_CONSTRAINT_CHECK = ResultCode.SQLITE_CONSTRAINT_CHECK
|
||||
public static let SQLITE_CONSTRAINT_COMMITHOOK = ResultCode.SQLITE_CONSTRAINT_COMMITHOOK
|
||||
public static let SQLITE_CONSTRAINT_FOREIGNKEY = ResultCode.SQLITE_CONSTRAINT_FOREIGNKEY
|
||||
public static let SQLITE_CONSTRAINT_FUNCTION = ResultCode.SQLITE_CONSTRAINT_FUNCTION
|
||||
public static let SQLITE_CONSTRAINT_NOTNULL = ResultCode.SQLITE_CONSTRAINT_NOTNULL
|
||||
public static let SQLITE_CONSTRAINT_PRIMARYKEY = ResultCode.SQLITE_CONSTRAINT_PRIMARYKEY
|
||||
public static let SQLITE_CONSTRAINT_TRIGGER = ResultCode.SQLITE_CONSTRAINT_TRIGGER
|
||||
public static let SQLITE_CONSTRAINT_UNIQUE = ResultCode.SQLITE_CONSTRAINT_UNIQUE
|
||||
public static let SQLITE_CONSTRAINT_VTAB = ResultCode.SQLITE_CONSTRAINT_VTAB
|
||||
public static let SQLITE_CONSTRAINT_ROWID = ResultCode.SQLITE_CONSTRAINT_ROWID
|
||||
public static let SQLITE_CONSTRAINT_PINNED = ResultCode.SQLITE_CONSTRAINT_PINNED
|
||||
public static let SQLITE_CONSTRAINT_DATATYPE = ResultCode.SQLITE_CONSTRAINT_DATATYPE
|
||||
public static let SQLITE_NOTICE_RECOVER_WAL = ResultCode.SQLITE_NOTICE_RECOVER_WAL
|
||||
public static let SQLITE_NOTICE_RECOVER_ROLLBACK = ResultCode.SQLITE_NOTICE_RECOVER_ROLLBACK
|
||||
public static let SQLITE_WARNING_AUTOINDEX = ResultCode.SQLITE_WARNING_AUTOINDEX
|
||||
public static let SQLITE_AUTH_USER = ResultCode.SQLITE_AUTH_USER
|
||||
public static let SQLITE_OK_LOAD_PERMANENTLY = ResultCode.SQLITE_OK_LOAD_PERMANENTLY
|
||||
public static let SQLITE_OK_SYMLINK = ResultCode.SQLITE_OK_SYMLINK
|
||||
}
|
||||
|
||||
extension DatabaseError {
|
||||
/// A boolean value indicating if the error has code
|
||||
/// `SQLITE_ABORT` or `SQLITE_INTERRUPT`.
|
||||
///
|
||||
/// Such an error can be thrown when a database has been interrupted, or
|
||||
/// when the database is suspended.
|
||||
///
|
||||
/// See ``DatabaseReader/interrupt()`` and ``Database/suspendNotification``
|
||||
/// for more information.
|
||||
public var isInterruptionError: Bool {
|
||||
switch resultCode {
|
||||
case .SQLITE_ABORT, .SQLITE_INTERRUPT:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseError: CustomStringConvertible {
|
||||
/// The error description.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// SQLite error 19: NOT NULL constraint failed: player.score
|
||||
/// - while executing `UPDATE player SET score = ? WHERE email = ?
|
||||
///
|
||||
/// The format of the error description may change between GRDB releases,
|
||||
/// without notice: don't have your application rely on any specific format.
|
||||
public var description: String {
|
||||
var description = "SQLite error \(resultCode.rawValue)"
|
||||
if let message {
|
||||
description += ": \(message)"
|
||||
}
|
||||
if let sql {
|
||||
description += " - while executing `\(sql.trimmedSQLStatement)`"
|
||||
}
|
||||
if publicStatementArguments, let arguments, !arguments.isEmpty {
|
||||
description += " with arguments \(arguments)"
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
/// The error description, where bound parameters, if present, are visible.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// SQLite error 19: NOT NULL constraint failed: player.score
|
||||
/// - while executing `UPDATE player SET score = ? WHERE email = ?
|
||||
/// with arguments [nil, "arthur@example.com"]
|
||||
///
|
||||
/// The format of the error description may change between GRDB releases,
|
||||
/// without notice: don't have your application rely on any specific format.
|
||||
///
|
||||
/// - warning: It is your responsibility to prevent sensitive
|
||||
/// information from leaking in unexpected locations, so use this
|
||||
/// property with care.
|
||||
public var expandedDescription: String {
|
||||
var description = "SQLite error \(resultCode.rawValue)"
|
||||
if let message {
|
||||
description += ": \(message)"
|
||||
}
|
||||
if let sql {
|
||||
description += " - while executing `\(sql.trimmedSQLStatement)`"
|
||||
}
|
||||
if let arguments, !arguments.isEmpty {
|
||||
description += " with arguments \(arguments)"
|
||||
}
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseError: CustomNSError {
|
||||
/// Part of the `CustomNSError` conformance.
|
||||
///
|
||||
/// Returns `GRDB.DatabaseError`.
|
||||
public static var errorDomain: String { "GRDB.DatabaseError" }
|
||||
|
||||
/// Part of the `CustomNSError` conformance.
|
||||
///
|
||||
/// Returns the ``extendedResultCode``.
|
||||
public var errorCode: Int { Int(extendedResultCode.rawValue) }
|
||||
|
||||
/// Part of the `CustomNSError` conformance.
|
||||
public var errorUserInfo: [String: Any] {
|
||||
var userInfo = [NSLocalizedDescriptionKey: description]
|
||||
if let message {
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = message
|
||||
}
|
||||
return userInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,482 @@
|
||||
/// A custom SQL function or aggregate.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating a Custom SQL Function or Aggregate
|
||||
///
|
||||
/// - ``init(_:argumentCount:pure:function:)``
|
||||
/// - ``init(_:argumentCount:pure:aggregate:)``
|
||||
/// - ``DatabaseAggregate``
|
||||
///
|
||||
/// ### Calling an SQL Function or Aggregate
|
||||
///
|
||||
/// - ``callAsFunction(_:)``
|
||||
///
|
||||
/// ### Built-in Functions
|
||||
///
|
||||
/// - ``capitalize``
|
||||
/// - ``localizedCapitalize``
|
||||
/// - ``localizedLowercase``
|
||||
/// - ``localizedUppercase``
|
||||
/// - ``lowercase``
|
||||
/// - ``uppercase``
|
||||
public final class DatabaseFunction: Hashable {
|
||||
// SQLite identifies functions by (name + argument count)
|
||||
private struct Identity: Hashable {
|
||||
let name: String
|
||||
let nArg: CInt // -1 for variadic functions
|
||||
}
|
||||
|
||||
/// The name of the SQL function
|
||||
public var name: String { identity.name }
|
||||
private let identity: Identity
|
||||
let isPure: Bool
|
||||
private let kind: Kind
|
||||
private var eTextRep: CInt { (SQLITE_UTF8 | (isPure ? SQLITE_DETERMINISTIC : 0)) }
|
||||
|
||||
/// Creates an SQL function.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let succ = DatabaseFunction("succ", argumentCount: 1) { dbValues in
|
||||
/// guard let int = Int.fromDatabaseValue(dbValues[0]) else {
|
||||
/// return nil
|
||||
/// }
|
||||
/// return int + 1
|
||||
/// }
|
||||
/// let dbQueue = try DatabaseQueue()
|
||||
/// try dbQueue.read { db in
|
||||
/// db.add(function: succ)
|
||||
/// try Int.fetchOne(db, sql: "SELECT succ(1)")! // 2
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Related APIs
|
||||
///
|
||||
/// - ``Database/add(function:)``
|
||||
///
|
||||
/// - parameters:
|
||||
/// - name: The function name.
|
||||
/// - argumentCount: The number of arguments of the function. If
|
||||
/// omitted, or nil, the function accepts any number of arguments.
|
||||
/// - pure: Whether the function is "pure", which means that its results
|
||||
/// only depends on its inputs. When a function is pure, SQLite has
|
||||
/// the opportunity to perform additional optimizations. Default value
|
||||
/// is false.
|
||||
/// - function: A function that takes an array of ``DatabaseValue``
|
||||
/// arguments, and returns an optional ``DatabaseValueConvertible``
|
||||
/// such as `Int`, `String`, `Date`, etc. The array is guaranteed to
|
||||
/// have exactly `argumentCount` elements, provided `argumentCount` is
|
||||
/// not nil.
|
||||
public init(
|
||||
_ name: String,
|
||||
argumentCount: Int? = nil,
|
||||
pure: Bool = false,
|
||||
function: @escaping ([DatabaseValue]) throws -> (any DatabaseValueConvertible)?)
|
||||
{
|
||||
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
|
||||
self.isPure = pure
|
||||
self.kind = .function{ (argc, argv) in
|
||||
let arguments = (0..<Int(argc)).map { index in
|
||||
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
|
||||
}
|
||||
return try function(arguments)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an SQL aggregate function.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct MySum: DatabaseAggregate {
|
||||
/// var sum: Int = 0
|
||||
///
|
||||
/// mutating func step(_ dbValues: [DatabaseValue]) {
|
||||
/// if let int = Int.fromDatabaseValue(dbValues[0]) {
|
||||
/// sum += int
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// func finalize() -> (any DatabaseValueConvertible)? {
|
||||
/// return sum
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let dbQueue = try DatabaseQueue()
|
||||
/// let mySum = DatabaseFunction("mySum", argumentCount: 1, aggregate: MySum.self)
|
||||
/// try dbQueue.write { db in
|
||||
/// db.add(function: mySum)
|
||||
/// try db.execute(sql: "CREATE TABLE test(i)")
|
||||
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
|
||||
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
|
||||
/// try Int.fetchOne(db, sql: "SELECT mySum(i) FROM test")! // 3
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Related APIs
|
||||
///
|
||||
/// - ``Database/add(function:)``
|
||||
///
|
||||
/// - parameters:
|
||||
/// - name: The function name.
|
||||
/// - argumentCount: The number of arguments of the aggregate. If
|
||||
/// omitted, or nil, the aggregate accepts any number of arguments.
|
||||
/// - pure: Whether the aggregate is "pure", which means that its
|
||||
/// results only depends on its inputs. When an aggregate is pure,
|
||||
/// SQLite has the opportunity to perform additional optimizations.
|
||||
/// Default value is false.
|
||||
/// - aggregate: A type that implements the ``DatabaseAggregate``
|
||||
/// protocol. For each step of the aggregation, its
|
||||
/// ``DatabaseAggregate/step(_:)`` method is called with an array of
|
||||
/// ``DatabaseValue`` arguments. The array is guaranteed to have
|
||||
/// exactly `argumentCount` elements, provided `argumentCount` is
|
||||
/// not nil.
|
||||
public init<Aggregate: DatabaseAggregate>(
|
||||
_ name: String,
|
||||
argumentCount: Int? = nil,
|
||||
pure: Bool = false,
|
||||
aggregate: Aggregate.Type)
|
||||
{
|
||||
self.identity = Identity(name: name, nArg: argumentCount.map(CInt.init) ?? -1)
|
||||
self.isPure = pure
|
||||
self.kind = .aggregate { Aggregate() }
|
||||
}
|
||||
|
||||
// TODO: GRDB7 -> expose ORDER BY and FILTER when we have distinct types for simple functions and aggregates.
|
||||
/// Returns an SQL expression that applies the function.
|
||||
///
|
||||
/// You can use a `DatabaseFunction` as a regular Swift function. It returns
|
||||
/// an SQL expression that you can use in the query interface.
|
||||
///
|
||||
/// In the example below, `square(Column("score"))` generates the
|
||||
/// `square(score)` SQL expression:
|
||||
///
|
||||
/// ```swift
|
||||
/// let square = DatabaseFunction("square", argumentCount: 1) { dbValues in
|
||||
/// guard let int = Int.fromDatabaseValue(dbValues[0]) else {
|
||||
/// return nil
|
||||
/// }
|
||||
/// return int * int
|
||||
/// }
|
||||
/// let dbQueue = try DatabaseQueue()
|
||||
/// try dbQueue.read { db in
|
||||
/// db.add(function: square)
|
||||
///
|
||||
/// // SELECT square(score) FROM player
|
||||
/// let squaredScores = let Player
|
||||
/// .select(square(Column("score")), as: Int.self)
|
||||
/// .fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
public func callAsFunction(_ arguments: any SQLExpressible...) -> SQLExpression {
|
||||
switch kind {
|
||||
case .function:
|
||||
return .simpleFunction(
|
||||
name,
|
||||
arguments.map(\.sqlExpression),
|
||||
isPure: isPure,
|
||||
isJSONValue: false)
|
||||
case .aggregate:
|
||||
return .aggregateFunction(
|
||||
name,
|
||||
arguments.map(\.sqlExpression),
|
||||
isDistinct: false,
|
||||
ordering: nil,
|
||||
filter: nil,
|
||||
isJSONValue: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls sqlite3_create_function_v2
|
||||
/// See <https://sqlite.org/c3ref/create_function.html>
|
||||
func install(in db: Database) {
|
||||
// Retain the function definition
|
||||
let definition = kind.definition
|
||||
let definitionP = Unmanaged.passRetained(definition).toOpaque()
|
||||
|
||||
let code = sqlite3_create_function_v2(
|
||||
db.sqliteConnection,
|
||||
identity.name,
|
||||
identity.nArg,
|
||||
eTextRep,
|
||||
definitionP,
|
||||
kind.xFunc,
|
||||
kind.xStep,
|
||||
kind.xFinal,
|
||||
{ definitionP in
|
||||
// Release the function definition
|
||||
Unmanaged<AnyObject>.fromOpaque(definitionP!).release()
|
||||
})
|
||||
|
||||
guard code == SQLITE_OK else {
|
||||
// Assume a GRDB bug: there is no point throwing any error.
|
||||
fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls sqlite3_create_function_v2
|
||||
/// See <https://sqlite.org/c3ref/create_function.html>
|
||||
func uninstall(in db: Database) {
|
||||
let code = sqlite3_create_function_v2(
|
||||
db.sqliteConnection,
|
||||
identity.name,
|
||||
identity.nArg,
|
||||
eTextRep,
|
||||
nil, nil, nil, nil, nil)
|
||||
|
||||
guard code == SQLITE_OK else {
|
||||
// Assume a GRDB bug: there is no point throwing any error.
|
||||
fatalError(DatabaseError(resultCode: code, message: db.lastErrorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
/// The way to compute the result of a function.
|
||||
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
|
||||
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
private class FunctionDefinition {
|
||||
let compute: (CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?
|
||||
init(compute: @escaping (CInt, UnsafeMutablePointer<OpaquePointer?>?)
|
||||
throws -> (any DatabaseValueConvertible)?)
|
||||
{
|
||||
self.compute = compute
|
||||
}
|
||||
}
|
||||
|
||||
/// The way to start an aggregate.
|
||||
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
|
||||
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
private class AggregateDefinition {
|
||||
let makeAggregate: () -> any DatabaseAggregate
|
||||
init(makeAggregate: @escaping () -> any DatabaseAggregate) {
|
||||
self.makeAggregate = makeAggregate
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of an aggregate, storable in SQLite
|
||||
private class AggregateContext {
|
||||
var aggregate: any DatabaseAggregate
|
||||
var hasErrored = false
|
||||
init(aggregate: some DatabaseAggregate) {
|
||||
self.aggregate = aggregate
|
||||
}
|
||||
}
|
||||
|
||||
/// A function kind: an "SQL function" or an "aggregate".
|
||||
/// See <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
private enum Kind {
|
||||
/// A regular function: SELECT f(1)
|
||||
case function((CInt, UnsafeMutablePointer<OpaquePointer?>?) throws -> (any DatabaseValueConvertible)?)
|
||||
|
||||
/// An aggregate: SELECT f(foo) FROM bar GROUP BY baz
|
||||
case aggregate(() -> any DatabaseAggregate)
|
||||
|
||||
/// Feeds the `pApp` parameter of sqlite3_create_function_v2
|
||||
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
var definition: AnyObject {
|
||||
switch self {
|
||||
case .function(let compute):
|
||||
return FunctionDefinition(compute: compute)
|
||||
case .aggregate(let makeAggregate):
|
||||
return AggregateDefinition(makeAggregate: makeAggregate)
|
||||
}
|
||||
}
|
||||
|
||||
/// Feeds the `xFunc` parameter of sqlite3_create_function_v2
|
||||
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
var xFunc: (@convention(c) (OpaquePointer?, CInt, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? {
|
||||
guard case .function = self else { return nil }
|
||||
return { (sqliteContext, argc, argv) in
|
||||
let definition = Unmanaged<FunctionDefinition>
|
||||
.fromOpaque(sqlite3_user_data(sqliteContext))
|
||||
.takeUnretainedValue()
|
||||
do {
|
||||
try DatabaseFunction.report(
|
||||
result: definition.compute(argc, argv),
|
||||
in: sqliteContext)
|
||||
} catch {
|
||||
DatabaseFunction.report(error: error, in: sqliteContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Feeds the `xStep` parameter of sqlite3_create_function_v2
|
||||
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
var xStep: (@convention(c) (OpaquePointer?, CInt, UnsafeMutablePointer<OpaquePointer?>?) -> Void)? {
|
||||
guard case .aggregate = self else { return nil }
|
||||
return { (sqliteContext, argc, argv) in
|
||||
let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext)
|
||||
let aggregateContext = aggregateContextU.takeUnretainedValue()
|
||||
assert(!aggregateContext.hasErrored) // assert SQLite behavior
|
||||
do {
|
||||
let arguments = (0..<Int(argc)).map { index in
|
||||
DatabaseValue(sqliteValue: argv.unsafelyUnwrapped[index]!)
|
||||
}
|
||||
try aggregateContext.aggregate.step(arguments)
|
||||
} catch {
|
||||
aggregateContext.hasErrored = true
|
||||
DatabaseFunction.report(error: error, in: sqliteContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Feeds the `xFinal` parameter of sqlite3_create_function_v2
|
||||
/// <http://sqlite.org/capi3ref.html#sqlite3_create_function>
|
||||
var xFinal: (@convention(c) (OpaquePointer?) -> Void)? {
|
||||
guard case .aggregate = self else { return nil }
|
||||
return { (sqliteContext) in
|
||||
let aggregateContextU = DatabaseFunction.unmanagedAggregateContext(sqliteContext)
|
||||
let aggregateContext = aggregateContextU.takeUnretainedValue()
|
||||
aggregateContextU.release()
|
||||
|
||||
guard !aggregateContext.hasErrored else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
try DatabaseFunction.report(
|
||||
result: aggregateContext.aggregate.finalize(),
|
||||
in: sqliteContext)
|
||||
} catch {
|
||||
DatabaseFunction.report(error: error, in: sqliteContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function that extracts the current state of an aggregate from an
|
||||
/// sqlite function execution context.
|
||||
///
|
||||
/// The result must be released when the aggregate concludes.
|
||||
///
|
||||
/// See <https://sqlite.org/c3ref/context.html>
|
||||
/// See <https://sqlite.org/c3ref/aggregate_context.html>
|
||||
private static func unmanagedAggregateContext(_ sqliteContext: OpaquePointer?) -> Unmanaged<AggregateContext> {
|
||||
// > The first time the sqlite3_aggregate_context(C,N) routine is called
|
||||
// > for a particular aggregate function, SQLite allocates N of memory,
|
||||
// > zeroes out that memory, and returns a pointer to the new memory.
|
||||
// > On second and subsequent calls to sqlite3_aggregate_context() for
|
||||
// > the same aggregate function instance, the same buffer is returned.
|
||||
let stride = MemoryLayout<Unmanaged<AggregateContext>>.stride
|
||||
let aggregateContextBufferP = UnsafeMutableRawBufferPointer(
|
||||
start: sqlite3_aggregate_context(sqliteContext, CInt(stride))!,
|
||||
count: stride)
|
||||
|
||||
if aggregateContextBufferP.contains(where: { $0 != 0 }) {
|
||||
// Buffer contains non-zero byte: load aggregate context
|
||||
let aggregateContextP = aggregateContextBufferP
|
||||
.baseAddress!
|
||||
.assumingMemoryBound(to: Unmanaged<AggregateContext>.self)
|
||||
return aggregateContextP.pointee
|
||||
} else {
|
||||
// Buffer contains null pointer: create aggregate context.
|
||||
let aggregate = Unmanaged<AggregateDefinition>.fromOpaque(sqlite3_user_data(sqliteContext))
|
||||
.takeUnretainedValue()
|
||||
.makeAggregate()
|
||||
let aggregateContext = AggregateContext(aggregate: aggregate)
|
||||
|
||||
// retain and store in SQLite's buffer
|
||||
let aggregateContextU = Unmanaged.passRetained(aggregateContext)
|
||||
let aggregateContextP = aggregateContextU.toOpaque()
|
||||
withUnsafeBytes(of: aggregateContextP) {
|
||||
aggregateContextBufferP.copyMemory(from: $0)
|
||||
}
|
||||
return aggregateContextU
|
||||
}
|
||||
}
|
||||
|
||||
private static func report(result: (any DatabaseValueConvertible)?, in sqliteContext: OpaquePointer?) {
|
||||
switch result?.databaseValue.storage ?? .null {
|
||||
case .null:
|
||||
sqlite3_result_null(sqliteContext)
|
||||
case .int64(let int64):
|
||||
sqlite3_result_int64(sqliteContext, int64)
|
||||
case .double(let double):
|
||||
sqlite3_result_double(sqliteContext, double)
|
||||
case .string(let string):
|
||||
sqlite3_result_text(sqliteContext, string, -1, SQLITE_TRANSIENT)
|
||||
case .blob(let data):
|
||||
data.withUnsafeBytes {
|
||||
sqlite3_result_blob(sqliteContext, $0.baseAddress, CInt($0.count), SQLITE_TRANSIENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func report(error: Error, in sqliteContext: OpaquePointer?) {
|
||||
if let error = error as? DatabaseError {
|
||||
if let message = error.message {
|
||||
sqlite3_result_error(sqliteContext, message, -1)
|
||||
}
|
||||
sqlite3_result_error_code(sqliteContext, error.extendedResultCode.rawValue)
|
||||
} else {
|
||||
sqlite3_result_error(sqliteContext, "\(error)", -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseFunction {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(identity)
|
||||
}
|
||||
|
||||
/// Two functions are equal if they share the same name and arity.
|
||||
public static func == (lhs: DatabaseFunction, rhs: DatabaseFunction) -> Bool {
|
||||
lhs.identity == rhs.identity
|
||||
}
|
||||
}
|
||||
|
||||
/// The protocol for custom SQLite aggregates.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct MySum : DatabaseAggregate {
|
||||
/// var sum: Int = 0
|
||||
///
|
||||
/// mutating func step(_ dbValues: [DatabaseValue]) {
|
||||
/// if let int = Int.fromDatabaseValue(dbValues[0]) {
|
||||
/// sum += int
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// func finalize() -> (any DatabaseValueConvertible)? {
|
||||
/// return sum
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let dbQueue = try DatabaseQueue()
|
||||
/// let mySum = DatabaseFunction("mySum", argumentCount: 1, aggregate: MySum.self)
|
||||
/// try dbQueue.write { db in
|
||||
/// db.add(function: mySum)
|
||||
/// try db.execute(sql: "CREATE TABLE test(i)")
|
||||
/// try db.execute(sql: "INSERT INTO test(i) VALUES (1)")
|
||||
/// try db.execute(sql: "INSERT INTO test(i) VALUES (2)")
|
||||
/// try Int.fetchOne(db, sql: "SELECT mysum(i) FROM test")! // 3
|
||||
/// }
|
||||
/// ```
|
||||
public protocol DatabaseAggregate {
|
||||
/// Creates an aggregate.
|
||||
///
|
||||
/// A new instance is created for each aggregation.
|
||||
init()
|
||||
|
||||
/// Updates the aggregated value for one step of the aggregation.
|
||||
///
|
||||
/// This method is called once for each step of the aggregation.
|
||||
///
|
||||
/// The `dbValues` argument contains as many values as given to the SQL
|
||||
/// aggregate function:
|
||||
///
|
||||
/// ```sql
|
||||
/// -- One value
|
||||
/// SELECT maxLength(name) FROM player
|
||||
///
|
||||
/// -- Two values
|
||||
/// SELECT maxFullNameLength(firstName, lastName) FROM player
|
||||
/// ```
|
||||
mutating func step(_ dbValues: [DatabaseValue]) throws
|
||||
|
||||
/// Returns the aggregated value.
|
||||
func finalize() throws -> (any DatabaseValueConvertible)?
|
||||
}
|
||||
@@ -0,0 +1,893 @@
|
||||
import Dispatch
|
||||
import Foundation
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public final class DatabasePool {
|
||||
private let writer: SerializedDatabase
|
||||
|
||||
/// The pool of reader connections.
|
||||
/// It is constant, until close() sets it to nil.
|
||||
private var readerPool: Pool<SerializedDatabase>?
|
||||
|
||||
@LockedBox var databaseSnapshotCount = 0
|
||||
|
||||
/// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers.
|
||||
private var suspensionObservers: [NSObjectProtocol] = []
|
||||
|
||||
// MARK: - Database Information
|
||||
|
||||
public var configuration: Configuration {
|
||||
writer.configuration
|
||||
}
|
||||
|
||||
/// The path to the database.
|
||||
public var path: String {
|
||||
writer.path
|
||||
}
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
/// Opens or creates an SQLite database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
|
||||
/// ```
|
||||
///
|
||||
/// The SQLite connections are closed when the database pool
|
||||
/// gets deallocated.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - path: The path to the database file.
|
||||
/// - configuration: A configuration.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public init(path: String, configuration: Configuration = Configuration()) throws {
|
||||
GRDBPrecondition(configuration.maximumReaderCount > 0, "configuration.maximumReaderCount must be at least 1")
|
||||
|
||||
// Writer
|
||||
writer = try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: configuration,
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "writer")
|
||||
|
||||
// Readers
|
||||
var readerConfiguration = DatabasePool.readerConfiguration(configuration)
|
||||
|
||||
// Readers can't allow dangling transactions because there's no
|
||||
// guarantee that one can get the same reader later in order to close
|
||||
// an opened transaction.
|
||||
readerConfiguration.allowsUnsafeTransactions = false
|
||||
|
||||
var readerCount = 0
|
||||
readerPool = Pool(
|
||||
maximumCount: configuration.maximumReaderCount,
|
||||
qos: configuration.readQoS,
|
||||
makeElement: {
|
||||
readerCount += 1 // protected by Pool (TODO: document this protection behavior)
|
||||
return try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: readerConfiguration,
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "reader.\(readerCount)")
|
||||
})
|
||||
|
||||
// Set up journal mode unless readonly
|
||||
if !configuration.readonly {
|
||||
switch configuration.journalMode {
|
||||
case .default, .wal:
|
||||
try writer.sync {
|
||||
try $0.setUpWALMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSuspension()
|
||||
|
||||
// Be a nice iOS citizen, and don't consume too much memory
|
||||
// See https://github.com/groue/GRDB.swift/#memory-management
|
||||
#if os(iOS)
|
||||
if configuration.automaticMemoryManagement {
|
||||
setupMemoryManagement()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Remove block-based Notification observers.
|
||||
suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||
|
||||
// Undo job done in setupMemoryManagement()
|
||||
//
|
||||
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error
|
||||
// Explicit unregistration is required before macOS 10.11.
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
|
||||
// Close reader connections before the writer connection.
|
||||
// Context: https://github.com/groue/GRDB.swift/issues/739
|
||||
readerPool = nil
|
||||
}
|
||||
|
||||
/// Returns a Configuration suitable for readonly connections on a
|
||||
/// WAL database.
|
||||
private static func readerConfiguration(_ configuration: Configuration) -> Configuration {
|
||||
var configuration = configuration
|
||||
|
||||
configuration.readonly = true
|
||||
|
||||
// Readers use deferred transactions by default.
|
||||
// Other transaction kinds are forbidden by SQLite in read-only connections.
|
||||
configuration.defaultTransactionKind = .deferred
|
||||
|
||||
// <https://www.sqlite.org/wal.html#sometimes_queries_return_sqlite_busy_in_wal_mode>
|
||||
// > But there are some obscure cases where a query against a WAL-mode
|
||||
// > database can return SQLITE_BUSY, so applications should be prepared
|
||||
// > for that happenstance.
|
||||
// >
|
||||
// > - If another database connection has the database mode open in
|
||||
// > exclusive locking mode [...]
|
||||
// > - When the last connection to a particular database is closing,
|
||||
// > that connection will acquire an exclusive lock for a short time
|
||||
// > while it cleans up the WAL and shared-memory files [...]
|
||||
// > - If the last connection to a database crashed, then the first new
|
||||
// > connection to open the database will start a recovery process. An
|
||||
// > exclusive lock is held during recovery. [...]
|
||||
//
|
||||
// The whole point of WAL readers is to avoid SQLITE_BUSY, so let's
|
||||
// setup a busy handler for pool readers, in order to workaround those
|
||||
// "obscure cases" that may happen when the database is shared between
|
||||
// multiple processes.
|
||||
if configuration.readonlyBusyMode == nil {
|
||||
configuration.readonlyBusyMode = .timeout(10)
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
}
|
||||
|
||||
// @unchecked because of databaseSnapshotCount, readerPool and suspensionObservers
|
||||
extension DatabasePool: @unchecked Sendable { }
|
||||
|
||||
extension DatabasePool {
|
||||
|
||||
// MARK: - Memory management
|
||||
|
||||
/// Frees as much memory as possible, by disposing non-essential memory.
|
||||
///
|
||||
/// This method is synchronous, and blocks the current thread until all
|
||||
/// database accesses are completed.
|
||||
///
|
||||
/// This method closes all read-only connections, unless the
|
||||
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
|
||||
/// is set.
|
||||
///
|
||||
/// - warning: This method can prevent concurrent reads from executing,
|
||||
/// until it returns. Prefer ``releaseMemoryEventually()`` if you intend
|
||||
/// to keep on using the database while releasing memory.
|
||||
public func releaseMemory() {
|
||||
// Release writer memory
|
||||
writer.sync { $0.releaseMemory() }
|
||||
|
||||
if configuration.persistentReadOnlyConnections {
|
||||
// Keep existing readers
|
||||
readerPool?.forEach { reader in
|
||||
reader.sync { $0.releaseMemory() }
|
||||
}
|
||||
} else {
|
||||
// Release readers memory by closing all connections.
|
||||
//
|
||||
// We must use a barrier in order to guarantee that memory has been
|
||||
// freed (reader connections closed) when the method exits, as
|
||||
// documented.
|
||||
//
|
||||
// Without the barrier, connections would only close _eventually_ (after
|
||||
// their eventual concurrent jobs have completed).
|
||||
readerPool?.barrier {
|
||||
readerPool?.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Eventually frees as much memory as possible, by disposing
|
||||
/// non-essential memory.
|
||||
///
|
||||
/// This method eventually closes all read-only connections, unless the
|
||||
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
|
||||
/// is set.
|
||||
///
|
||||
/// Unlike ``releaseMemory()``, this method does not prevent concurrent
|
||||
/// database accesses when it is executing. But it does not notify when
|
||||
/// non-essential memory has been freed.
|
||||
public func releaseMemoryEventually() {
|
||||
if configuration.persistentReadOnlyConnections {
|
||||
// Keep existing readers
|
||||
readerPool?.forEach { reader in
|
||||
reader.async { $0.releaseMemory() }
|
||||
}
|
||||
} else {
|
||||
// Release readers memory by eventually closing all reader connections
|
||||
// (they will close after their current jobs have completed).
|
||||
readerPool?.removeAll()
|
||||
}
|
||||
|
||||
// Release writer memory eventually.
|
||||
writer.async { $0.releaseMemory() }
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
/// Listens to UIApplicationDidEnterBackgroundNotification and
|
||||
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
|
||||
/// as much memory as possible.
|
||||
private func setupMemoryManagement() {
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(DatabasePool.applicationDidReceiveMemoryWarning(_:)),
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(DatabasePool.applicationDidEnterBackground(_:)),
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidEnterBackground(_ notification: NSNotification) {
|
||||
guard let application = notification.object as? UIApplication else {
|
||||
return
|
||||
}
|
||||
|
||||
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
|
||||
if task == .invalid {
|
||||
// Release memory synchronously
|
||||
releaseMemory()
|
||||
} else {
|
||||
// Release memory eventually.
|
||||
//
|
||||
// We don't know when reader connections will be closed (because
|
||||
// they may be currently in use), so we don't quite know when
|
||||
// reader memory will be freed (which would be the ideal timing for
|
||||
// ending our background task).
|
||||
//
|
||||
// So let's just end the background task after the writer connection
|
||||
// has freed its memory. That's better than nothing.
|
||||
releaseMemoryEventually()
|
||||
writer.async { _ in
|
||||
application.endBackgroundTask(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
|
||||
releaseMemoryEventually()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension DatabasePool: DatabaseReader {
|
||||
|
||||
public func close() throws {
|
||||
try readerPool?.barrier {
|
||||
// Close writer connection first. If we can't close it,
|
||||
// don't close readers.
|
||||
//
|
||||
// This allows us to exit this method as fully closed (read and
|
||||
// writes fail), or not closed at all (reads and writes succeed).
|
||||
//
|
||||
// Unfortunately, this introduces a regression for
|
||||
// https://github.com/groue/GRDB.swift/issues/739.
|
||||
// TODO: fix this regression.
|
||||
try writer.sync { try $0.close() }
|
||||
|
||||
// OK writer is closed. Now close readers and
|
||||
// eventually prevent any future read access
|
||||
defer { readerPool = nil }
|
||||
|
||||
try readerPool?.forEach { reader in
|
||||
try reader.sync { try $0.close() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interrupting Database Operations
|
||||
|
||||
public func interrupt() {
|
||||
writer.interrupt()
|
||||
readerPool?.forEach { $0.interrupt() }
|
||||
}
|
||||
|
||||
// MARK: - Database Suspension
|
||||
|
||||
func suspend() {
|
||||
if configuration.readonly {
|
||||
// read-only WAL connections can't acquire locks and do not need to
|
||||
// be suspended.
|
||||
return
|
||||
}
|
||||
writer.suspend()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
if configuration.readonly {
|
||||
// read-only WAL connections can't acquire locks and do not need to
|
||||
// be suspended.
|
||||
return
|
||||
}
|
||||
writer.resume()
|
||||
}
|
||||
|
||||
private func setupSuspension() {
|
||||
if configuration.observesSuspensionNotifications {
|
||||
let center = NotificationCenter.default
|
||||
suspensionObservers.append(center.addObserver(
|
||||
forName: Database.suspendNotification,
|
||||
object: nil,
|
||||
queue: nil,
|
||||
using: { [weak self] _ in self?.suspend() }
|
||||
))
|
||||
suspensionObservers.append(center.addObserver(
|
||||
forName: Database.resumeNotification,
|
||||
object: nil,
|
||||
queue: nil,
|
||||
using: { [weak self] _ in self?.resume() }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reading from Database
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func read<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.get { reader in
|
||||
try reader.sync { db in
|
||||
try db.isolated {
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
value(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
do {
|
||||
let (reader, releaseReader) = try result.get()
|
||||
// Second async jump because that's how `Pool.async` has to be used.
|
||||
reader.async { db in
|
||||
defer {
|
||||
try? db.commit() // Ignore commit error
|
||||
releaseReader(.reuse)
|
||||
}
|
||||
do {
|
||||
// The block isolation comes from the DEFERRED transaction.
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
GRDBPrecondition(currentReader == nil, "Database methods are not reentrant.")
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.get { reader in
|
||||
try reader.sync { db in
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
value(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
do {
|
||||
let (reader, releaseReader) = try result.get()
|
||||
// Second async jump because that's how `Pool.async` has to be used.
|
||||
reader.async { db in
|
||||
defer {
|
||||
releaseReader(.reuse)
|
||||
}
|
||||
do {
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
if let reader = currentReader {
|
||||
return try reader.reentrantSync(value)
|
||||
} else if writer.onValidQueue {
|
||||
return try writer.execute(value)
|
||||
} else {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.get { reader in
|
||||
try reader.sync { db in
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
|
||||
// The semaphore that blocks until futureResult is defined:
|
||||
let futureSemaphore = DispatchSemaphore(value: 0)
|
||||
var futureResult: Result<T, Error>? = nil
|
||||
|
||||
asyncConcurrentRead { dbResult in
|
||||
// Fetch and release the future
|
||||
futureResult = dbResult.flatMap { db in Result { try value(db) } }
|
||||
futureSemaphore.signal()
|
||||
}
|
||||
|
||||
return DatabaseFuture {
|
||||
// Block the future until results are fetched
|
||||
_ = futureSemaphore.wait(timeout: .distantFuture)
|
||||
return try futureResult!.get()
|
||||
}
|
||||
}
|
||||
|
||||
public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
asyncConcurrentRead(value)
|
||||
}
|
||||
|
||||
/// Performs an asynchronous read access.
|
||||
///
|
||||
/// This method must be called from the writer dispatch queue, outside of
|
||||
/// any transaction. You'll get a fatal error otherwise.
|
||||
///
|
||||
/// The `value` function is guaranteed to see the database in the last
|
||||
/// committed state at the moment this method is called. Eventual
|
||||
/// concurrent database updates are not visible from the function.
|
||||
///
|
||||
/// This method returns as soon as the isolation guarantee described above
|
||||
/// has been established.
|
||||
///
|
||||
/// In the example below, the number of players is fetched concurrently with
|
||||
/// the player insertion. Yet it is guaranteed to be zero:
|
||||
///
|
||||
/// ```swift
|
||||
/// try writer.asyncWriteWithoutTransaction { db in
|
||||
/// // Delete all players
|
||||
/// try Player.deleteAll()
|
||||
///
|
||||
/// // Count players concurrently
|
||||
/// writer.asyncConcurrentRead { dbResult in
|
||||
/// do {
|
||||
/// let db = try dbResult.get()
|
||||
/// // Guaranteed to be zero
|
||||
/// let count = try Player.fetchCount(db)
|
||||
/// } catch {
|
||||
/// // Handle error
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Insert a player
|
||||
/// try Player(...).insert(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter value: A function that accesses the database.
|
||||
public func asyncConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
// Check that we're on the writer queue...
|
||||
writer.execute { db in
|
||||
// ... and that no transaction is opened.
|
||||
GRDBPrecondition(!db.isInsideTransaction, """
|
||||
must not be called from inside a transaction. \
|
||||
If this error is raised from a DatabasePool.write block, use \
|
||||
DatabasePool.writeWithoutTransaction instead (and use \
|
||||
transactions when needed).
|
||||
""")
|
||||
}
|
||||
|
||||
// The semaphore that blocks the writing dispatch queue until snapshot
|
||||
// isolation has been established:
|
||||
let isolationSemaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
do {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
reader.async { db in
|
||||
defer {
|
||||
try? db.commit() // Ignore commit error
|
||||
releaseReader(.reuse)
|
||||
}
|
||||
do {
|
||||
// https://www.sqlite.org/isolation.html
|
||||
//
|
||||
// > In WAL mode, SQLite exhibits "snapshot isolation". When
|
||||
// > a read transaction starts, that reader continues to see
|
||||
// > an unchanging "snapshot" of the database file as it
|
||||
// > existed at the moment in time when the read transaction
|
||||
// > started. Any write transactions that commit while the
|
||||
// > read transaction is active are still invisible to the
|
||||
// > read transaction, because the reader is seeing a
|
||||
// > snapshot of database file from a prior moment in time.
|
||||
//
|
||||
// That's exactly what we need. But what does "when read
|
||||
// transaction starts" mean?
|
||||
//
|
||||
// http://www.sqlite.org/lang_transaction.html
|
||||
//
|
||||
// > Deferred [transaction] means that no locks are acquired
|
||||
// > on the database until the database is first accessed.
|
||||
// > [...] Locks are not acquired until the first read or
|
||||
// > write operation. [...] Because the acquisition of locks
|
||||
// > is deferred until they are needed, it is possible that
|
||||
// > another thread or process could create a separate
|
||||
// > transaction and write to the database after the BEGIN
|
||||
// > on the current thread has executed.
|
||||
//
|
||||
// Now that's precise enough: SQLite defers snapshot
|
||||
// isolation until the first SELECT:
|
||||
//
|
||||
// Reader Writer
|
||||
// BEGIN DEFERRED TRANSACTION
|
||||
// UPDATE ... (1)
|
||||
// Here the change (1) is visible from the reader
|
||||
// SELECT ...
|
||||
// UPDATE ... (2)
|
||||
// Here the change (2) is not visible from the reader
|
||||
//
|
||||
// We thus have to perform a select that establishes the
|
||||
// snapshot isolation before we release the writer queue:
|
||||
//
|
||||
// Reader Writer
|
||||
// BEGIN DEFERRED TRANSACTION
|
||||
// SELECT anything
|
||||
// UPDATE ... (1)
|
||||
// Here the change (1) is not visible from the reader
|
||||
//
|
||||
// Since any select goes, use `PRAGMA schema_version`.
|
||||
try db.beginTransaction(.deferred)
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
} catch {
|
||||
isolationSemaphore.signal()
|
||||
value(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
// Now that we have an isolated snapshot of the last commit, we
|
||||
// can release the writer queue.
|
||||
isolationSemaphore.signal()
|
||||
|
||||
value(.success(db))
|
||||
}
|
||||
} catch {
|
||||
isolationSemaphore.signal()
|
||||
value(.failure(error))
|
||||
}
|
||||
|
||||
// Block the writer queue until snapshot isolation success or error
|
||||
_ = isolationSemaphore.wait(timeout: .distantFuture)
|
||||
}
|
||||
|
||||
/// Invalidates open read-only SQLite connections.
|
||||
///
|
||||
/// After this method is called, read-only database access methods will use
|
||||
/// new SQLite connections.
|
||||
///
|
||||
/// Eventual concurrent read-only accesses are not interrupted, and
|
||||
/// proceed until completion.
|
||||
///
|
||||
/// - This method closes all read-only connections, even if the
|
||||
/// ``Configuration/persistentReadOnlyConnections`` configuration flag
|
||||
/// is set.
|
||||
public func invalidateReadOnlyConnections() {
|
||||
readerPool?.removeAll()
|
||||
}
|
||||
|
||||
/// Returns a reader that can be used from the current dispatch queue,
|
||||
/// if any.
|
||||
private var currentReader: SerializedDatabase? {
|
||||
guard let readerPool else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var readers: [SerializedDatabase] = []
|
||||
readerPool.forEach { reader in
|
||||
// We can't check for reader.onValidQueue here because
|
||||
// Pool.forEach() runs its closure argument in some arbitrary
|
||||
// dispatch queue. We thus extract the reader so that we can query
|
||||
// it below.
|
||||
readers.append(reader)
|
||||
}
|
||||
|
||||
// Now the readers array contains some readers. The pool readers may
|
||||
// already be different, because some other thread may have started
|
||||
// a new read, for example.
|
||||
//
|
||||
// This doesn't matter: the reader we are looking for is already on
|
||||
// its own dispatch queue. If it exists, is still in use, thus still
|
||||
// in the pool, and thus still relevant for our check:
|
||||
return readers.first { $0.onValidQueue }
|
||||
}
|
||||
|
||||
// MARK: - WAL Snapshot Transactions
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// Returns a long-lived WAL snapshot transaction on a reader connection.
|
||||
func walSnapshotTransaction() throws -> WALSnapshotTransaction {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
|
||||
let (reader, releaseReader) = try readerPool.get()
|
||||
return try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
|
||||
// Discard the connection if the transaction could not be
|
||||
// properly ended. If we'd reuse it, the next read would
|
||||
// fail because we'd fail starting a read transaction.
|
||||
releaseReader(isInsideTransaction ? .discard : .reuse)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a long-lived WAL snapshot transaction on a reader connection.
|
||||
///
|
||||
/// - important: The `completion` argument is executed in a serial
|
||||
/// dispatch queue, so make sure you use the transaction asynchronously.
|
||||
func asyncWALSnapshotTransaction(_ completion: @escaping (Result<WALSnapshotTransaction, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
completion(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
|
||||
readerPool.asyncGet { result in
|
||||
completion(result.flatMap { reader, releaseReader in
|
||||
Result {
|
||||
try WALSnapshotTransaction(onReader: reader, release: { isInsideTransaction in
|
||||
// Discard the connection if the transaction could not be
|
||||
// properly ended. If we'd reuse it, the next read would
|
||||
// fail because we'd fail starting a read transaction.
|
||||
releaseReader(isInsideTransaction ? .discard : .reuse)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - Database Observation
|
||||
|
||||
public func _add<Reducer: ValueReducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
{
|
||||
if configuration.readonly {
|
||||
// The easy case: the database does not change
|
||||
return _addReadOnly(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
|
||||
} else if observation.requiresWriteAccess {
|
||||
// Observe from the writer database connection.
|
||||
return _addWriteOnly(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
|
||||
} else {
|
||||
// DatabasePool can perform concurrent observation
|
||||
return _addConcurrent(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
}
|
||||
}
|
||||
|
||||
/// A concurrent observation fetches the initial value without waiting for
|
||||
/// the writer.
|
||||
private func _addConcurrent<Reducer: ValueReducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
{
|
||||
assert(!configuration.readonly, "Use _addReadOnly(observation:) instead")
|
||||
assert(!observation.requiresWriteAccess, "Use _addWriteOnly(observation:) instead")
|
||||
let observer = ValueConcurrentObserver(
|
||||
dbPool: self,
|
||||
scheduler: scheduler,
|
||||
trackingMode: observation.trackingMode,
|
||||
reducer: observation.makeReducer(),
|
||||
events: observation.events,
|
||||
onChange: onChange)
|
||||
return observer.start()
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabasePool: DatabaseWriter {
|
||||
// MARK: - Writing in Database
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func writeWithoutTransaction<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func barrierWriteWithoutTransaction<T>(_ updates: (Database) throws -> T) throws -> T {
|
||||
guard let readerPool else {
|
||||
throw DatabaseError.connectionIsClosed()
|
||||
}
|
||||
return try readerPool.barrier {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
|
||||
guard let readerPool else {
|
||||
updates(.failure(DatabaseError.connectionIsClosed()))
|
||||
return
|
||||
}
|
||||
readerPool.asyncBarrier {
|
||||
self.writer.sync { updates(.success($0)) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps database operations inside a database transaction.
|
||||
///
|
||||
/// The `updates` function runs in the writer dispatch queue, serialized
|
||||
/// with all database updates.
|
||||
///
|
||||
/// If `updates` throws an error, the transaction is rollbacked and the
|
||||
/// error is rethrown. If it returns
|
||||
/// ``Database/TransactionCompletion/rollback``, the transaction is also
|
||||
/// rollbacked, but no error is thrown.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbPool.writeInTransaction { db in
|
||||
/// try Player(name: "Arthur").insert(db)
|
||||
/// try Player(name: "Barbara").insert(db)
|
||||
/// return .commit
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - precondition: This method is not reentrant.
|
||||
/// - parameters:
|
||||
/// - kind: The transaction type (default nil). If nil, the transaction
|
||||
/// type is the ``Configuration/defaultTransactionKind`` of the
|
||||
/// the ``configuration``.
|
||||
/// - updates: A function that updates the database.
|
||||
/// - throws: The error thrown by `updates`, or by the wrapping transaction.
|
||||
public func writeInTransaction(
|
||||
_ kind: Database.TransactionKind? = nil,
|
||||
_ updates: (Database) throws -> Database.TransactionCompletion)
|
||||
throws
|
||||
{
|
||||
try writer.sync { db in
|
||||
try db.inTransaction(kind) {
|
||||
try updates(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.reentrantSync(updates)
|
||||
}
|
||||
|
||||
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
|
||||
writer.async(updates)
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabasePool {
|
||||
|
||||
// MARK: - Snapshots
|
||||
|
||||
/// Creates a database snapshot that serializes accesses to an unchanging
|
||||
/// database content, as it exists at the moment the snapshot is created.
|
||||
///
|
||||
/// It is a programmer error to create a snapshot from the writer dispatch
|
||||
/// queue when a transaction is opened:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbPool.write { db in
|
||||
/// try Player.deleteAll()
|
||||
///
|
||||
/// // fatal error: makeSnapshot() must not be called from inside a transaction
|
||||
/// let snapshot = try dbPool.makeSnapshot()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To avoid this fatal error, create the snapshot *before* or *after*
|
||||
/// the transaction:
|
||||
///
|
||||
/// ```swift
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
///
|
||||
/// try dbPool.writeWithoutTransaction { db in
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
///
|
||||
/// try db.inTransaction {
|
||||
/// try Player.deleteAll()
|
||||
/// return .commit
|
||||
/// }
|
||||
///
|
||||
/// // OK
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
/// }
|
||||
///
|
||||
/// let snapshot = try dbPool.makeSnapshot() // OK
|
||||
/// ```
|
||||
public func makeSnapshot() throws -> DatabaseSnapshot {
|
||||
// Sanity check
|
||||
if writer.onValidQueue {
|
||||
writer.execute { db in
|
||||
GRDBPrecondition(
|
||||
!db.isInsideTransaction,
|
||||
"makeSnapshot() must not be called from inside a transaction.")
|
||||
}
|
||||
}
|
||||
|
||||
return try DatabaseSnapshot(
|
||||
path: path,
|
||||
configuration: DatabasePool.readerConfiguration(writer.configuration),
|
||||
defaultLabel: "GRDB.DatabasePool",
|
||||
purpose: "snapshot.\($databaseSnapshotCount.increment())")
|
||||
}
|
||||
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// Creates a database snapshot that allows concurrent accesses to an
|
||||
/// unchanging database content, as it exists at the moment the snapshot
|
||||
/// is created.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// A ``DatabaseError`` of code `SQLITE_ERROR` is thrown if the SQLite
|
||||
/// database is not in the [WAL mode](https://www.sqlite.org/wal.html),
|
||||
/// or if this method is called from a write transaction, or if the
|
||||
/// wal file is missing or truncated (size zero).
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/snapshot_get.html>
|
||||
public func makeSnapshotPool() throws -> DatabaseSnapshotPool {
|
||||
try unsafeReentrantRead { db in
|
||||
try DatabaseSnapshotPool(db)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#if canImport(Combine)
|
||||
/// A namespace for database Combine publishers.
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public enum DatabasePublishers { }
|
||||
#endif
|
||||
@@ -0,0 +1,524 @@
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
public final class DatabaseQueue {
|
||||
private let writer: SerializedDatabase
|
||||
|
||||
/// If Database Suspension is enabled, this array contains the necessary `NotificationCenter` observers.
|
||||
private var suspensionObservers: [NSObjectProtocol] = []
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
public var configuration: Configuration {
|
||||
writer.configuration
|
||||
}
|
||||
|
||||
public var path: String {
|
||||
writer.path
|
||||
}
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Opens or creates an SQLite database.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
|
||||
/// ```
|
||||
///
|
||||
/// The SQLite connection is closed when the database queue
|
||||
/// gets deallocated.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - path: The path to the database file.
|
||||
/// - configuration: A configuration.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public init(path: String, configuration: Configuration = Configuration()) throws {
|
||||
// DatabaseQueue can't perform parallel reads
|
||||
var configuration = configuration
|
||||
configuration.maximumReaderCount = 1
|
||||
|
||||
writer = try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: configuration,
|
||||
defaultLabel: "GRDB.DatabaseQueue")
|
||||
|
||||
// Set up journal mode unless readonly
|
||||
if !configuration.readonly {
|
||||
switch configuration.journalMode {
|
||||
case .default:
|
||||
break
|
||||
case .wal:
|
||||
try writer.sync {
|
||||
try $0.setUpWALMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSuspension()
|
||||
|
||||
// Be a nice iOS citizen, and don't consume too much memory
|
||||
// See https://github.com/groue/GRDB.swift/#memory-management
|
||||
#if os(iOS)
|
||||
if configuration.automaticMemoryManagement {
|
||||
setupMemoryManagement()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Opens an in-memory SQLite database.
|
||||
///
|
||||
/// To create an independent in-memory database, don't pass any name. The
|
||||
/// database memory is released when the database queue is deallocated:
|
||||
///
|
||||
/// ```swift
|
||||
/// // An independent in-memory database
|
||||
/// let dbQueue = try DatabaseQueue()
|
||||
/// ```
|
||||
///
|
||||
/// When you need to open several connections to the same in-memory
|
||||
/// database, give it a name:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A shared in-memory database
|
||||
/// let dbQueue = try DatabaseQueue(named: "myDatabase")
|
||||
/// ```
|
||||
///
|
||||
/// In this case, the database is automatically deleted and memory is
|
||||
/// reclaimed when the last connection to the database of the given
|
||||
/// name closes.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/inmemorydb.html>
|
||||
///
|
||||
/// - parameter name: When nil, an independent in-memory database opens.
|
||||
/// Otherwise, the shared in-memory database of the given name opens.
|
||||
/// - parameter configuration: A configuration.
|
||||
public init(named name: String? = nil, configuration: Configuration = Configuration()) throws {
|
||||
let path: String
|
||||
if let name {
|
||||
path = "file:\(name)?mode=memory&cache=shared"
|
||||
} else {
|
||||
path = ":memory:"
|
||||
}
|
||||
|
||||
writer = try SerializedDatabase(
|
||||
path: path,
|
||||
configuration: configuration,
|
||||
defaultLabel: "GRDB.DatabaseQueue")
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Remove block-based Notification observers.
|
||||
suspensionObservers.forEach(NotificationCenter.default.removeObserver(_:))
|
||||
|
||||
// Undo job done in setupMemoryManagement()
|
||||
//
|
||||
// https://developer.apple.com/library/mac/releasenotes/Foundation/RN-Foundation/index.html#10_11Error
|
||||
// Explicit unregistration is required before macOS 10.11.
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
}
|
||||
|
||||
// @unchecked because of suspensionObservers
|
||||
extension DatabaseQueue: @unchecked Sendable { }
|
||||
|
||||
extension DatabaseQueue {
|
||||
|
||||
// MARK: - Memory management
|
||||
|
||||
/// Free as much memory as possible.
|
||||
///
|
||||
/// This method blocks the current thread until all database accesses are completed.
|
||||
public func releaseMemory() {
|
||||
writer.sync { $0.releaseMemory() }
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
/// Listens to UIApplicationDidEnterBackgroundNotification and
|
||||
/// UIApplicationDidReceiveMemoryWarningNotification in order to release
|
||||
/// as much memory as possible.
|
||||
private func setupMemoryManagement() {
|
||||
let center = NotificationCenter.default
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(DatabaseQueue.applicationDidReceiveMemoryWarning(_:)),
|
||||
name: UIApplication.didReceiveMemoryWarningNotification,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(DatabaseQueue.applicationDidEnterBackground(_:)),
|
||||
name: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidEnterBackground(_ notification: NSNotification) {
|
||||
guard let application = notification.object as? UIApplication else {
|
||||
return
|
||||
}
|
||||
|
||||
let task: UIBackgroundTaskIdentifier = application.beginBackgroundTask(expirationHandler: nil)
|
||||
if task == .invalid {
|
||||
// Release memory synchronously
|
||||
releaseMemory()
|
||||
} else {
|
||||
// Release memory asynchronously
|
||||
writer.async { db in
|
||||
db.releaseMemory()
|
||||
application.endBackgroundTask(task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidReceiveMemoryWarning(_ notification: NSNotification) {
|
||||
writer.async { db in
|
||||
db.releaseMemory()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension DatabaseQueue: DatabaseReader {
|
||||
public func close() throws {
|
||||
try writer.sync { try $0.close() }
|
||||
}
|
||||
|
||||
// MARK: - Interrupting Database Operations
|
||||
|
||||
public func interrupt() {
|
||||
writer.interrupt()
|
||||
}
|
||||
|
||||
// MARK: - Database Suspension
|
||||
|
||||
func suspend() {
|
||||
writer.suspend()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
writer.resume()
|
||||
}
|
||||
|
||||
private func setupSuspension() {
|
||||
if configuration.observesSuspensionNotifications {
|
||||
let center = NotificationCenter.default
|
||||
suspensionObservers.append(center.addObserver(
|
||||
forName: Database.suspendNotification,
|
||||
object: nil,
|
||||
queue: nil,
|
||||
using: { [weak self] _ in self?.suspend() }
|
||||
))
|
||||
suspensionObservers.append(center.addObserver(
|
||||
forName: Database.resumeNotification,
|
||||
object: nil,
|
||||
queue: nil,
|
||||
using: { [weak self] _ in self?.resume() }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reading from Database
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func read<T>(_ value: (Database) throws -> T) throws -> T {
|
||||
try writer.sync { db in
|
||||
try db.isolated(readOnly: true) {
|
||||
try value(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
writer.async { db in
|
||||
defer {
|
||||
// Ignore error because we can not notify it.
|
||||
try? db.commit()
|
||||
try? db.endReadOnly()
|
||||
}
|
||||
|
||||
do {
|
||||
// Enter read-only mode before starting a transaction, so that the
|
||||
// transaction commit does not trigger database observation.
|
||||
// See <https://github.com/groue/GRDB.swift/pull/1213>.
|
||||
try db.beginReadOnly()
|
||||
try db.beginTransaction(.deferred)
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func unsafeRead<T>(_ value: (Database) throws -> T) rethrows -> T {
|
||||
try writer.sync(value)
|
||||
}
|
||||
|
||||
public func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
writer.async { value(.success($0)) }
|
||||
}
|
||||
|
||||
public func unsafeReentrantRead<T>(_ value: (Database) throws -> T) rethrows -> T {
|
||||
try writer.reentrantSync(value)
|
||||
}
|
||||
|
||||
public func concurrentRead<T>(_ value: @escaping (Database) throws -> T) -> DatabaseFuture<T> {
|
||||
// DatabaseQueue can't perform parallel reads.
|
||||
// Perform a blocking read instead.
|
||||
return DatabaseFuture(Result {
|
||||
// Check that we're on the writer queue, as documented
|
||||
try writer.execute { db in
|
||||
try db.isolated(readOnly: true) {
|
||||
try value(db)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func spawnConcurrentRead(_ value: @escaping (Result<Database, Error>) -> Void) {
|
||||
// Check that we're on the writer queue...
|
||||
writer.execute { db in
|
||||
// ... and that no transaction is opened.
|
||||
GRDBPrecondition(!db.isInsideTransaction, "must not be called from inside a transaction.")
|
||||
|
||||
defer {
|
||||
// Ignore error because we can not notify it.
|
||||
try? db.commit()
|
||||
try? db.endReadOnly()
|
||||
}
|
||||
|
||||
do {
|
||||
// Enter read-only mode before starting a transaction, so that the
|
||||
// transaction commit does not trigger database observation.
|
||||
// See <https://github.com/groue/GRDB.swift/pull/1213>.
|
||||
try db.beginReadOnly()
|
||||
try db.beginTransaction(.deferred)
|
||||
value(.success(db))
|
||||
} catch {
|
||||
value(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Database Observation
|
||||
|
||||
public func _add<Reducer: ValueReducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
{
|
||||
if configuration.readonly {
|
||||
// The easy case: the database does not change
|
||||
return _addReadOnly(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
} else {
|
||||
// Observe from the writer database connection.
|
||||
return _addWriteOnly(
|
||||
observation: observation,
|
||||
scheduling: scheduler,
|
||||
onChange: onChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseQueue: DatabaseWriter {
|
||||
// MARK: - Writing in Database
|
||||
|
||||
/// Wraps database operations inside a database transaction.
|
||||
///
|
||||
/// The `updates` function runs in the writer dispatch queue, serialized
|
||||
/// with all database updates.
|
||||
///
|
||||
/// If `updates` throws an error, the transaction is rollbacked and the
|
||||
/// error is rethrown. If it returns
|
||||
/// ``Database/TransactionCompletion/rollback``, the transaction is also
|
||||
/// rollbacked, but no error is thrown.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.inTransaction { db in
|
||||
/// try Player(name: "Arthur").insert(db)
|
||||
/// try Player(name: "Barbara").insert(db)
|
||||
/// return .commit
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - kind: The transaction type (default nil). If nil, the transaction
|
||||
/// type is the ``Configuration/defaultTransactionKind`` of the
|
||||
/// the ``configuration``.
|
||||
/// - updates: A function that updates the database.
|
||||
/// - throws: The error thrown by `updates`, or by the wrapping transaction.
|
||||
public func inTransaction(
|
||||
_ kind: Database.TransactionKind? = nil,
|
||||
_ updates: (Database) throws -> Database.TransactionCompletion)
|
||||
throws
|
||||
{
|
||||
try writer.sync { db in
|
||||
try db.inTransaction(kind) {
|
||||
try updates(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func writeWithoutTransaction<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
public func barrierWriteWithoutTransaction<T>(_ updates: (Database) throws -> T) throws -> T {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
|
||||
public func asyncBarrierWriteWithoutTransaction(_ updates: @escaping (Result<Database, Error>) -> Void) {
|
||||
writer.async { updates(.success($0)) }
|
||||
}
|
||||
|
||||
/// Executes database operations, and returns their result after they have
|
||||
/// finished executing.
|
||||
///
|
||||
/// This method is identical to
|
||||
/// ``DatabaseWriter/writeWithoutTransaction(_:)-4qh1w``
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let newPlayerCount = try dbQueue.inDatabase { db in
|
||||
/// try Player(name: "Arthur").insert(db)
|
||||
/// return try Player.fetchCount(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Database operations run in the writer dispatch queue, serialized
|
||||
/// with all database updates performed by this `DatabaseWriter`.
|
||||
///
|
||||
/// The ``Database`` argument to `updates` is valid only during the
|
||||
/// execution of the closure. Do not store or return the database connection
|
||||
/// for later use.
|
||||
///
|
||||
/// It is a programmer error to call this method from another database
|
||||
/// access method. Doing so raises a "Database methods are not reentrant"
|
||||
/// fatal error at runtime.
|
||||
///
|
||||
/// - warning: Database operations are not wrapped in a transaction. They
|
||||
/// can see changes performed by concurrent writes or writes performed by
|
||||
/// other processes: two identical requests performed by the `updates`
|
||||
/// closure may not return the same value. Concurrent database accesses
|
||||
/// can see partial updates performed by the `updates` closure.
|
||||
///
|
||||
/// - parameter updates: A closure which accesses the database.
|
||||
/// - throws: The error thrown by `updates`.
|
||||
public func inDatabase<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.sync(updates)
|
||||
}
|
||||
|
||||
public func unsafeReentrantWrite<T>(_ updates: (Database) throws -> T) rethrows -> T {
|
||||
try writer.reentrantSync(updates)
|
||||
}
|
||||
|
||||
public func asyncWriteWithoutTransaction(_ updates: @escaping (Database) -> Void) {
|
||||
writer.async(updates)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Temp Copy
|
||||
|
||||
extension DatabaseQueue {
|
||||
/// Returns a connection to an in-memory copy of the database at `path`.
|
||||
///
|
||||
/// Changes performed on the returned connection do not impact the
|
||||
/// original database at `path`.
|
||||
///
|
||||
/// The database memory is released when the returned connection
|
||||
/// is deallocated.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let path = "/path/to/database.sqlite"
|
||||
/// let dbQueue = try DatabaseQueue.inMemoryCopy(fromPath: path)
|
||||
/// ```
|
||||
public static func inMemoryCopy(
|
||||
fromPath path: String,
|
||||
configuration: Configuration = Configuration())
|
||||
throws -> DatabaseQueue
|
||||
{
|
||||
var sourceConfig = configuration
|
||||
sourceConfig.readonly = true
|
||||
let source = try DatabaseQueue(path: path, configuration: sourceConfig)
|
||||
|
||||
var copyConfig = configuration
|
||||
copyConfig.readonly = false
|
||||
let result = try DatabaseQueue(configuration: copyConfig)
|
||||
|
||||
try source.backup(to: result)
|
||||
|
||||
if configuration.readonly {
|
||||
// Result was not opened read-only so that we could perform the
|
||||
// copy. And SQLITE_OPEN_READONLY has no effect on in-memory
|
||||
// databases anyway.
|
||||
//
|
||||
// So let's simulate read-only with PRAGMA query_only.
|
||||
try result.inDatabase { db in
|
||||
try db.beginReadOnly()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Returns a connection to a private, temporary, on-disk copy of the
|
||||
/// database at `path`.
|
||||
///
|
||||
/// Changes performed on the returned connection do not impact the
|
||||
/// original database at `path`.
|
||||
///
|
||||
/// The on-disk copy will be automatically deleted from disk as soon as
|
||||
/// the returned connection is closed or deallocated.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let path = "/path/to/database.sqlite"
|
||||
/// let dbQueue = try DatabaseQueue.temporaryCopy(fromPath: path)
|
||||
/// ```
|
||||
public static func temporaryCopy(
|
||||
fromPath path: String,
|
||||
configuration: Configuration = Configuration())
|
||||
throws -> DatabaseQueue
|
||||
{
|
||||
var sourceConfig = configuration
|
||||
sourceConfig.readonly = true
|
||||
let source = try DatabaseQueue(path: path, configuration: sourceConfig)
|
||||
|
||||
// <https://www.sqlite.org/c3ref/open.html>
|
||||
// > If the filename is an empty string, then a private, temporary
|
||||
// > on-disk database will be created. This private database will be
|
||||
// > automatically deleted as soon as the database connection
|
||||
// > is closed.
|
||||
var copyConfig = configuration
|
||||
copyConfig.readonly = false
|
||||
let result = try DatabaseQueue(path: "", configuration: copyConfig)
|
||||
|
||||
try source.backup(to: result)
|
||||
|
||||
if configuration.readonly {
|
||||
// Result was not opened read-only so that we could perform the
|
||||
// copy. So let's simulate read-only with PRAGMA query_only.
|
||||
try result.inDatabase { db in
|
||||
try db.beginReadOnly()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
#endif
|
||||
import Dispatch
|
||||
|
||||
/// A type that reads from an SQLite database.
|
||||
///
|
||||
/// Do not declare new conformances to `DatabaseReader`. Only the built-in
|
||||
/// conforming types are valid.
|
||||
///
|
||||
/// The protocol comes with isolation guarantees that describe the behavior of
|
||||
/// conforming types in a multithreaded application. See <doc:Concurrency> for
|
||||
/// more information.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Database Information
|
||||
///
|
||||
/// - ``configuration``
|
||||
/// - ``path``
|
||||
///
|
||||
/// ### Reading from the Database
|
||||
///
|
||||
/// - ``read(_:)-3806d``
|
||||
/// - ``read(_:)-4w6gy``
|
||||
/// - ``readPublisher(receiveOn:value:)``
|
||||
/// - ``asyncRead(_:)``
|
||||
///
|
||||
/// ### Unsafe Methods
|
||||
///
|
||||
/// - ``unsafeRead(_:)-5i7tf``
|
||||
/// - ``unsafeRead(_:)-11mk0``
|
||||
/// - ``unsafeReentrantRead(_:)``
|
||||
/// - ``asyncUnsafeRead(_:)``
|
||||
///
|
||||
/// ### Printing Database Content
|
||||
///
|
||||
/// - ``dumpContent(format:to:)``
|
||||
/// - ``dumpRequest(_:format:to:)``
|
||||
/// - ``dumpSchema(to:)``
|
||||
/// - ``dumpSQL(_:format:to:)``
|
||||
/// - ``dumpTables(_:format:tableHeader:stableOrder:to:)``
|
||||
/// - ``DumpFormat``
|
||||
/// - ``DumpTableHeaderOptions``
|
||||
///
|
||||
/// ### Other Database Operations
|
||||
///
|
||||
/// - ``backup(to:pagesPerStep:progress:)``
|
||||
/// - ``close()``
|
||||
/// - ``interrupt()``
|
||||
///
|
||||
/// ### Supporting Types
|
||||
///
|
||||
/// - ``AnyDatabaseReader``
|
||||
public protocol DatabaseReader: AnyObject, Sendable {
|
||||
|
||||
/// The database configuration.
|
||||
var configuration: Configuration { get }
|
||||
|
||||
/// The path to the database file.
|
||||
///
|
||||
/// In-memory databases also have a path:
|
||||
/// see [In-Memory Databases](https://www.sqlite.org/inmemorydb.html).
|
||||
var path: String { get }
|
||||
|
||||
/// Closes the database connection.
|
||||
///
|
||||
/// - note: You do not have to call this method, and you should not call
|
||||
/// it unless the correct execution of your program depends on precise
|
||||
/// database closing. Database connections are automatically closed when
|
||||
/// they are deinitialized, and this is sufficient for most applications.
|
||||
///
|
||||
/// If this method does not throw, then the database is properly closed, and
|
||||
/// every future database access will throw a ``DatabaseError`` of
|
||||
/// code `SQLITE_MISUSE`.
|
||||
///
|
||||
/// Otherwise, there exists concurrent database accesses or living prepared
|
||||
/// statements that prevent the database from closing, and this method
|
||||
/// throws a ``DatabaseError`` of code `SQLITE_BUSY`.
|
||||
/// See <https://www.sqlite.org/c3ref/close.html> for more information.
|
||||
///
|
||||
/// After an error has been thrown, the database may still be opened, and
|
||||
/// you can keep on accessing it. It may also remain in a "zombie" state,
|
||||
/// in which case it will throw `SQLITE_MISUSE` for all future
|
||||
/// database accesses.
|
||||
///
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
func close() throws
|
||||
|
||||
// MARK: - Interrupting Database Operations
|
||||
|
||||
/// Causes any pending database operation to abort and return at its
|
||||
/// earliest opportunity.
|
||||
///
|
||||
/// This method can be called from any thread.
|
||||
///
|
||||
/// A call to `interrupt()` that occurs when there are no running SQL
|
||||
/// statements is a no-op and has no effect on SQL statements that are
|
||||
/// started after `interrupt()` returns.
|
||||
///
|
||||
/// A database operation that is interrupted will throw a ``DatabaseError``
|
||||
/// with code `SQLITE_INTERRUPT`. If the interrupted SQL operation is an
|
||||
/// `INSERT`, `UPDATE`, or `DELETE` that is inside an explicit transaction,
|
||||
/// then the entire transaction will be rolled back automatically. If the
|
||||
/// rolled back transaction was started by a transaction-wrapping method
|
||||
/// such as ``DatabaseWriter/write(_:)-76inz`` or
|
||||
/// ``Database/inTransaction(_:_:)``, then all database accesses will throw
|
||||
/// a ``DatabaseError`` with code `SQLITE_ABORT` until the wrapping
|
||||
/// method returns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// // interrupted:
|
||||
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
|
||||
/// // not executed:
|
||||
/// try Player(...).insert(db)
|
||||
/// } // throws SQLITE_INTERRUPT
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// do {
|
||||
/// // interrupted:
|
||||
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
|
||||
/// } catch { }
|
||||
/// try Player(...).insert(db) // throws SQLITE_ABORT
|
||||
/// } // throws SQLITE_ABORT
|
||||
///
|
||||
/// try dbQueue.write { db in
|
||||
/// do {
|
||||
/// // interrupted:
|
||||
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
|
||||
/// } catch { }
|
||||
/// } // throws SQLITE_ABORT
|
||||
/// ```
|
||||
///
|
||||
/// Beware: when an application opens a transaction without a
|
||||
/// transaction-wrapping method, no `SQLITE_ABORT` error warns of
|
||||
/// aborted transactions:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
|
||||
/// try db.beginTransaction()
|
||||
/// do {
|
||||
/// // interrupted:
|
||||
/// try Player(...).insert(db) // throws SQLITE_INTERRUPT
|
||||
/// } catch { }
|
||||
/// try Player(...).insert(db) // success
|
||||
/// try db.commit() // throws SQLITE_ERROR "cannot commit - no transaction is active"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Both `SQLITE_ABORT` and `SQLITE_INTERRUPT` errors can be checked with the
|
||||
/// ``DatabaseError/isInterruptionError`` property.
|
||||
func interrupt()
|
||||
|
||||
// MARK: - Read From Database
|
||||
|
||||
/// Executes read-only database operations, and returns their result after
|
||||
/// they have finished executing.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let count = try reader.read { db in
|
||||
/// try Player.fetchCount(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Database operations are isolated in a transaction: they do not see
|
||||
/// changes performed by eventual concurrent writes (even writes performed
|
||||
/// by other processes).
|
||||
///
|
||||
/// The database connection is read-only: attempts to write throw a
|
||||
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
|
||||
///
|
||||
/// The ``Database`` argument to `value` is valid only during the execution
|
||||
/// of the closure. Do not store or return the database connection for
|
||||
/// later use.
|
||||
///
|
||||
/// It is a programmer error to call this method from another database
|
||||
/// access method. Doing so raises a "Database methods are not reentrant"
|
||||
/// fatal error at runtime.
|
||||
///
|
||||
/// - parameter value: A closure which accesses the database.
|
||||
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
|
||||
/// would happen while establishing the database access.
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
func read<T>(_ value: (Database) throws -> T) throws -> T
|
||||
|
||||
/// Schedules read-only database operations for execution, and
|
||||
/// returns immediately.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try reader.asyncRead { dbResult in
|
||||
/// do {
|
||||
/// let db = try dbResult.get()
|
||||
/// let count = try Player.fetchCount(db)
|
||||
/// } catch {
|
||||
/// // Handle error
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Database operations are isolated in a transaction: they do not see
|
||||
/// changes performed by eventual concurrent writes (even writes performed
|
||||
/// by other processes).
|
||||
///
|
||||
/// The database connection is read-only: attempts to write throw a
|
||||
/// ``DatabaseError`` with resultCode `SQLITE_READONLY`.
|
||||
///
|
||||
/// - parameter value: A closure which accesses the database. Its argument
|
||||
/// is a `Result` that provides the database connection, or the failure
|
||||
/// that would prevent establishing the read access to the database.
|
||||
func asyncRead(_ value: @escaping (Result<Database, Error>) -> Void)
|
||||
|
||||
/// Executes database operations, and returns their result after they have
|
||||
/// finished executing.
|
||||
///
|
||||
/// This method is "unsafe" because the database reader does nothing more
|
||||
/// than providing a database connection. When you use this method, you
|
||||
/// become responsible for the thread-safety of your application, and
|
||||
/// responsible for database accesses performed by other processes. See
|
||||
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
|
||||
/// more information.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let count = try reader.unsafeRead { db in
|
||||
/// try Player.fetchCount(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The ``Database`` argument to `value` is valid only during the execution
|
||||
/// of the closure. Do not store or return the database connection for
|
||||
/// later use.
|
||||
///
|
||||
/// It is a programmer error to call this method from another database
|
||||
/// access method. Doing so raises a "Database methods are not reentrant"
|
||||
/// fatal error at runtime.
|
||||
///
|
||||
/// - warning: Database operations may not be wrapped in a transaction. They
|
||||
/// may see changes performed by concurrent writes or writes performed by
|
||||
/// other processes: two identical requests performed by the `value`
|
||||
/// closure may not return the same value.
|
||||
/// - warning: Attempts to write in the database may succeed.
|
||||
///
|
||||
/// - parameter value: A closure which accesses the database.
|
||||
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
|
||||
/// would happen while establishing the database access.
|
||||
@_disfavoredOverload // SR-15150 Async overloading in protocol implementation fails
|
||||
func unsafeRead<T>(_ value: (Database) throws -> T) throws -> T
|
||||
|
||||
/// Schedules database operations for execution, and returns immediately.
|
||||
///
|
||||
/// This method is "unsafe" because the database reader does nothing more
|
||||
/// than providing a database connection. When you use this method, you
|
||||
/// become responsible for the thread-safety of your application, and
|
||||
/// responsible for database accesses performed by other processes. See
|
||||
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
|
||||
/// more information.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// reader.asyncUnsafeRead { dbResult in
|
||||
/// do {
|
||||
/// let db = try dbResult.get()
|
||||
/// let count = try Player.fetchCount(db)
|
||||
/// } catch {
|
||||
/// // handle error
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - warning: Database operations may not be wrapped in a transaction. They
|
||||
/// may see changes performed by concurrent writes or writes performed by
|
||||
/// other processes: two identical requests performed by the `value`
|
||||
/// closure may not return the same value.
|
||||
/// - warning: Attempts to write in the database may succeed.
|
||||
///
|
||||
/// - parameter value: A closure which accesses the database. Its argument
|
||||
/// is a `Result` that provides the database connection, or the failure
|
||||
/// that would prevent establishing the read access to the database.
|
||||
func asyncUnsafeRead(_ value: @escaping (Result<Database, Error>) -> Void)
|
||||
|
||||
/// Executes database operations, and returns their result after they have
|
||||
/// finished executing.
|
||||
///
|
||||
/// This method is "unsafe" because the database reader does nothing more
|
||||
/// than providing a database connection. When you use this method, you
|
||||
/// become responsible for the thread-safety of your application, and
|
||||
/// responsible for database accesses performed by other processes. See
|
||||
/// <doc:Concurrency#Safe-and-Unsafe-Database-Accesses> for
|
||||
/// more information.
|
||||
///
|
||||
/// This method can be called from other database access methods. If called
|
||||
/// from the dispatch queue of a current database access (read or write),
|
||||
/// the `Database` argument to `value` is the same as the current
|
||||
/// database access.
|
||||
///
|
||||
/// Reentrant database accesses are discouraged because they muddle
|
||||
/// transaction boundaries
|
||||
/// (see <doc:Concurrency#Rule-2:-Mind-your-transactions> for
|
||||
/// more information).
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let count = try reader.unsafeReentrantRead { db in
|
||||
/// try Player.fetchCount(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The ``Database`` argument to `value` is valid only during the execution
|
||||
/// of the closure. Do not store or return the database connection for
|
||||
/// later use.
|
||||
///
|
||||
/// - warning: Database operations may not be wrapped in a transaction. They
|
||||
/// may see changes performed by concurrent writes or writes performed by
|
||||
/// other processes: two identical requests performed by the `value`
|
||||
/// closure may not return the same value.
|
||||
/// - warning: Attempts to write in the database may succeed.
|
||||
///
|
||||
/// - parameter value: A closure which accesses the database.
|
||||
/// - throws: The error thrown by `value`, or any ``DatabaseError`` that
|
||||
/// would happen while establishing the database access.
|
||||
func unsafeReentrantRead<T>(_ value: (Database) throws -> T) throws -> T
|
||||
|
||||
|
||||
// MARK: - Value Observation
|
||||
|
||||
/// Starts a value observation.
|
||||
///
|
||||
/// Use the ``ValueObservation/start(in:scheduling:onError:onChange:)``
|
||||
/// method instead.
|
||||
///
|
||||
/// - parameter observation: a ValueObservation.
|
||||
/// - returns: A DatabaseCancellable that can stop the observation.
|
||||
func _add<Reducer: ValueReducer>(
|
||||
observation: ValueObservation<Reducer>,
|
||||
scheduling scheduler: some ValueObservationScheduler,
|
||||
onChange: @escaping (Reducer.Value) -> Void)
|
||||
-> AnyDatabaseCancellable
|
||||
}
|
||||
|
||||
extension DatabaseReader {
|
||||
|
||||
// MARK: - Backup
|
||||
|
||||
/// Copies the database contents into another database.
|
||||
///
|
||||
/// The `backup` method blocks the current thread until the destination
|
||||
/// database contains the same contents as the source database.
|
||||
///
|
||||
/// When the source is a DatabasePool, concurrent writes can happen during
|
||||
/// the backup. Those writes may, or may not, be reflected in the backup,
|
||||
/// but they won't trigger any error.
|
||||
///
|
||||
/// Usage:
|
||||
///
|
||||
/// ```swift
|
||||
/// let source: DatabaseQueue = ...
|
||||
/// let destination: DatabaseQueue = ...
|
||||
/// try source.backup(to: destination)
|
||||
/// ```
|
||||
///
|
||||
/// When you're after progress reporting during backup, you'll want to
|
||||
/// perform the backup in several steps. Each step copies the number of
|
||||
/// _database pages_ you specify. See <https://www.sqlite.org/c3ref/backup_finish.html>
|
||||
/// for more information:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Backup with progress reporting
|
||||
/// try source.backup(to: destination, pagesPerStep: ...) { progress in
|
||||
/// print("Database backup progress:", progress)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The `progress` callback will be called at least 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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,311 @@
|
||||
// MARK: - FetchRequest
|
||||
|
||||
/// A type that fetches and decodes database rows.
|
||||
///
|
||||
/// The main kinds of fetch requests are ``SQLRequest``
|
||||
/// and ``QueryInterfaceRequest``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // SQLRequest
|
||||
/// let request: SQLRequest<Player> = """
|
||||
/// SELECT * FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// // QueryInterfaceRequest
|
||||
/// let request = Player.filter(Column("lastName") == lastName)
|
||||
///
|
||||
/// // Use the request
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try request.fetchAll(db) // [Player]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Counting the Results
|
||||
///
|
||||
/// - ``fetchCount(_:)``
|
||||
///
|
||||
/// ### Fetching Database Rows
|
||||
///
|
||||
/// - ``fetchCursor(_:)-9283d``
|
||||
/// - ``fetchAll(_:)-7p809``
|
||||
/// - ``fetchOne(_:)-9fafl``
|
||||
/// - ``fetchSet(_:)-6bdrd``
|
||||
///
|
||||
/// ### Fetching Database Values
|
||||
///
|
||||
/// - ``fetchCursor(_:)-19f5g``
|
||||
/// - ``fetchCursor(_:)-66xoi``
|
||||
/// - ``fetchAll(_:)-1loau``
|
||||
/// - ``fetchAll(_:)-28pne``
|
||||
/// - ``fetchOne(_:)-44mvv``
|
||||
/// - ``fetchOne(_:)-5hlkf``
|
||||
/// - ``fetchSet(_:)-4hhtm``
|
||||
/// - ``fetchSet(_:)-9wshm``
|
||||
///
|
||||
/// ### Fetching Records
|
||||
///
|
||||
/// - ``fetchCursor(_:)-2ah3q``
|
||||
/// - ``fetchAll(_:)-vdos``
|
||||
/// - ``fetchOne(_:)-2bq0k``
|
||||
/// - ``fetchSet(_:)-4jdrq``
|
||||
///
|
||||
/// ### Preparing Database Requests
|
||||
///
|
||||
/// - ``makePreparedRequest(_:forSingleResult:)``
|
||||
/// - ``PreparedRequest``
|
||||
///
|
||||
/// ### Adapting the Fetched Rows
|
||||
///
|
||||
/// - ``adapted(_:)``
|
||||
/// - ``AdaptedFetchRequest``
|
||||
///
|
||||
/// ### Supporting Types
|
||||
///
|
||||
/// - ``AnyFetchRequest``
|
||||
public protocol FetchRequest<RowDecoder>: SQLSubqueryable, DatabaseRegionConvertible {
|
||||
/// The type that tells how fetched database rows should be interpreted.
|
||||
associatedtype RowDecoder
|
||||
|
||||
/// Returns a ``PreparedRequest``.
|
||||
///
|
||||
/// The `singleResult` argument is a hint that a single result row will be
|
||||
/// consumed. Implementations can optionally use it to optimize the
|
||||
/// prepared statement, for example by adding a `LIMIT 1` SQL clause:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Calls makePreparedRequest(db, forSingleResult: true)
|
||||
/// try request.fetchOne(db)
|
||||
///
|
||||
/// // Calls makePreparedRequest(db, forSingleResult: false)
|
||||
/// try request.fetchAll(db)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter singleResult: A hint that a single result row will be
|
||||
/// consumed.
|
||||
func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest
|
||||
|
||||
/// Returns the number of rows fetched by the request.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
func fetchCount(_ db: Database) throws -> Int
|
||||
}
|
||||
|
||||
extension FetchRequest {
|
||||
/// Returns the database region that the request feeds from.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
public func databaseRegion(_ db: Database) throws -> DatabaseRegion {
|
||||
try makePreparedRequest(db, forSingleResult: false).statement.databaseRegion
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PreparedRequest
|
||||
|
||||
/// A closure executed before a supplementary fetch is performed.
|
||||
///
|
||||
/// Support for `Database.dumpRequest`.
|
||||
///
|
||||
/// - parameter request: The supplementary request
|
||||
/// - parameter keyPath: The key path target of the supplementary fetch.
|
||||
typealias WillExecuteSupplementaryRequest = (_ request: AnyFetchRequest<Row>, _ keyPath: [String]) throws -> Void
|
||||
|
||||
/// A closure that performs supplementary fetches.
|
||||
///
|
||||
/// Support for eager loading of hasMany associations.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - parameter rows: The rows that are modified by the supplementary fetch.
|
||||
/// - parameter willExecuteSupplementaryRequest: A closure to execute before
|
||||
/// performing supplementary fetches.
|
||||
typealias SupplementaryFetch = (
|
||||
_ db: Database,
|
||||
_ rows: [Row],
|
||||
_ willExecuteSupplementaryRequest: WillExecuteSupplementaryRequest?)
|
||||
throws -> Void
|
||||
|
||||
/// A `PreparedRequest` is a request that is ready to be executed.
|
||||
public struct PreparedRequest {
|
||||
/// A prepared statement with bound parameters.
|
||||
public var statement: Statement
|
||||
|
||||
/// An eventual adapter for rows fetched by the select statement.
|
||||
public var adapter: (any RowAdapter)?
|
||||
|
||||
/// A closure that performs supplementary fetches.
|
||||
/// Support for eager loading of hasMany associations.
|
||||
var supplementaryFetch: SupplementaryFetch?
|
||||
|
||||
init(
|
||||
statement: Statement,
|
||||
adapter: (any RowAdapter)?,
|
||||
supplementaryFetch: SupplementaryFetch? = nil)
|
||||
{
|
||||
self.statement = statement
|
||||
self.adapter = adapter
|
||||
self.supplementaryFetch = supplementaryFetch
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit non-conformance to Sendable: `PreparedRequest` contains
|
||||
// a statement.
|
||||
@available(*, unavailable)
|
||||
extension PreparedRequest: Sendable { }
|
||||
|
||||
extension PreparedRequest: Refinable { }
|
||||
|
||||
// MARK: - AdaptedFetchRequest
|
||||
|
||||
extension FetchRequest {
|
||||
/// Returns an adapted request.
|
||||
///
|
||||
/// The returned request performs an identical database query, but adapts
|
||||
/// the fetched rows. See ``RowAdapter``, and
|
||||
/// ``splittingRowAdapters(columnCounts:)`` for a sample code that uses
|
||||
/// `adapted(_:)`.
|
||||
///
|
||||
/// - parameter adapter: A closure that accepts a database connection and
|
||||
/// returns a row adapter.
|
||||
public func adapted(_ adapter: @escaping (Database) throws -> any RowAdapter) -> AdaptedFetchRequest<Self> {
|
||||
AdaptedFetchRequest(self, adapter)
|
||||
}
|
||||
}
|
||||
|
||||
/// An adapted request.
|
||||
///
|
||||
/// See ``FetchRequest/adapted(_:)``.
|
||||
public struct AdaptedFetchRequest<Base: FetchRequest> {
|
||||
let base: Base
|
||||
let adapter: (Database) throws -> any RowAdapter
|
||||
|
||||
/// Creates an adapted request from a base request and a closure that builds
|
||||
/// a row adapter from a database connection.
|
||||
init(_ base: Base, _ adapter: @escaping (Database) throws -> any RowAdapter) {
|
||||
self.base = base
|
||||
self.adapter = adapter
|
||||
}
|
||||
}
|
||||
|
||||
extension AdaptedFetchRequest: SQLSubqueryable {
|
||||
public var sqlSubquery: SQLSubquery {
|
||||
base.sqlSubquery
|
||||
}
|
||||
}
|
||||
|
||||
extension AdaptedFetchRequest: FetchRequest {
|
||||
public typealias RowDecoder = Base.RowDecoder
|
||||
|
||||
public func fetchCount(_ db: Database) throws -> Int {
|
||||
try base.fetchCount(db)
|
||||
}
|
||||
|
||||
public func makePreparedRequest(
|
||||
_ db: Database,
|
||||
forSingleResult singleResult: Bool = false)
|
||||
throws -> PreparedRequest
|
||||
{
|
||||
var preparedRequest = try base.makePreparedRequest(db, forSingleResult: singleResult)
|
||||
|
||||
if let baseAdapter = preparedRequest.adapter {
|
||||
preparedRequest.adapter = try ChainedAdapter(first: baseAdapter, second: adapter(db))
|
||||
} else {
|
||||
preparedRequest.adapter = try adapter(db)
|
||||
}
|
||||
|
||||
return preparedRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AnyFetchRequest
|
||||
|
||||
/// A type-erased FetchRequest.
|
||||
///
|
||||
/// An `AnyFetchRequest` forwards its operations to an underlying request,
|
||||
/// hiding its specifics.
|
||||
public struct AnyFetchRequest<RowDecoder> {
|
||||
private let request: FetchRequestEraser
|
||||
|
||||
/// Returns a request that performs an identical database query, but decodes
|
||||
/// database rows with `type`.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // AnyFetchRequest<Player>
|
||||
/// let playerRequest = AnyFetchRequest(Player.all())
|
||||
///
|
||||
/// // AnyFetchRequest<Row>
|
||||
/// let rowRequest = playerRequest.asRequest(of: Row.self)
|
||||
public func asRequest<T>(of type: T.Type) -> AnyFetchRequest<T> {
|
||||
AnyFetchRequest<T>(request: request)
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyFetchRequest {
|
||||
/// Creates a request that wraps and forwards operations to `request`.
|
||||
public init(_ request: some FetchRequest<RowDecoder>) {
|
||||
self.init(request: ConcreteFetchRequestEraser(request: request))
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyFetchRequest: SQLSubqueryable {
|
||||
public var sqlSubquery: SQLSubquery {
|
||||
request.sqlSubquery
|
||||
}
|
||||
}
|
||||
|
||||
extension AnyFetchRequest: FetchRequest {
|
||||
public func fetchCount(_ db: Database) throws -> Int {
|
||||
try request.fetchCount(db)
|
||||
}
|
||||
|
||||
public func makePreparedRequest(
|
||||
_ db: Database,
|
||||
forSingleResult singleResult: Bool = false)
|
||||
throws -> PreparedRequest
|
||||
{
|
||||
try request.makePreparedRequest(db, forSingleResult: singleResult)
|
||||
}
|
||||
}
|
||||
|
||||
// Class-based type erasure, so that we preserve full type information.
|
||||
private class FetchRequestEraser: FetchRequest {
|
||||
typealias RowDecoder = Void
|
||||
|
||||
var sqlSubquery: SQLSubquery {
|
||||
fatalError("subclass must override")
|
||||
}
|
||||
|
||||
func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest {
|
||||
fatalError("subclass must override")
|
||||
}
|
||||
|
||||
func fetchCount(_ db: Database) throws -> Int {
|
||||
fatalError("subclass must override")
|
||||
}
|
||||
}
|
||||
|
||||
private final class ConcreteFetchRequestEraser<Request: FetchRequest>: FetchRequestEraser {
|
||||
let request: Request
|
||||
|
||||
init(request: Request) {
|
||||
self.request = request
|
||||
}
|
||||
|
||||
override var sqlSubquery: SQLSubquery {
|
||||
request.sqlSubquery
|
||||
}
|
||||
|
||||
override func fetchCount(_ db: Database) throws -> Int {
|
||||
try request.fetchCount(db)
|
||||
}
|
||||
|
||||
override func makePreparedRequest(_ db: Database, forSingleResult singleResult: Bool) throws -> PreparedRequest {
|
||||
try request.makePreparedRequest(db, forSingleResult: singleResult)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,751 @@
|
||||
import Foundation
|
||||
|
||||
/// Returns an array of row adapters that split a row according to the
|
||||
/// provided numbers of columns.
|
||||
///
|
||||
/// This method is useful for splitting a row into chunks.
|
||||
///
|
||||
/// For example, let's consider the following SQL query:
|
||||
///
|
||||
/// ```swift
|
||||
/// let sql = """
|
||||
/// SELECT player.*, team.*
|
||||
/// FROM player
|
||||
/// LEFT JOIN team ON team.id = player.teamId
|
||||
/// WHERE player.id = ?
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// The resulting rows contains columns from both player and team tables:
|
||||
///
|
||||
/// ```swift
|
||||
/// // [id: 1, name: "Arthur", teamId: 42, id: 42, name: "Reds"]
|
||||
/// // <---------------------------------><-------------------->
|
||||
/// // player columns team columns
|
||||
/// let row = try Row.fetchOne(db, sql: sql, arguments: [1])
|
||||
/// ```
|
||||
///
|
||||
/// Because some columns have the same name (`id` and `name`), it is
|
||||
/// difficult to access the team columns.
|
||||
///
|
||||
/// `splittingRowAdapters` and ``ScopeAdapter`` make it possible to
|
||||
/// access player and team columns independently, with row ``Row/scopes``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapters = try splittingRowAdapters([
|
||||
/// db.columns(in: "player").count,
|
||||
/// db.columns(in: "team").count,
|
||||
/// ])
|
||||
/// let adapter = ScopeAdapter([
|
||||
/// "player": adapters[0],
|
||||
/// "team": adapters[1],
|
||||
/// ])
|
||||
/// if let row = try Row.fetchOne(db, sql: sql, arguments: [1], adapter: adapter) {
|
||||
/// // A Row that only contains player columns
|
||||
/// // [id: 1, name: "Arthur", teamId: 42]
|
||||
/// row.scopes["player"]
|
||||
///
|
||||
/// // A Row that only contains team columns
|
||||
/// // [id: 42, name: "Reds"]
|
||||
/// row.scopes["team"]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Decoding ``FetchableRecord`` types is easy:
|
||||
///
|
||||
/// ```swift
|
||||
/// if let row = try Row.fetchOne(db, sql: sql, arguments: [1], adapter: adapter) {
|
||||
/// // Player(id: 1, name: "Arthur", teamId: 42)
|
||||
/// let player: Player = row["player"]
|
||||
///
|
||||
/// // Team(id: 42, name: "Reds")
|
||||
/// // nil if the LEFT JOIN has fetched NULL team columns
|
||||
/// if let team: Team? = row["team"]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You can package this technique in a dedicated type, as in the next
|
||||
/// example. It enhances the previous sample codes with:
|
||||
///
|
||||
/// - Support for record types that customize their fetched columns
|
||||
/// with ``TableRecord/databaseSelection-7iphs``.
|
||||
/// - ``SQLRequest`` and its support for [SQL Interpolation](https://github.com/groue/GRDB.swift/blob/master/Documentation/SQLInterpolation.md).
|
||||
/// - ``FetchRequest/adapted(_:)`` for building a request that embeds the
|
||||
/// row adapters.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: TableRecord, FetchableRecord { ... }
|
||||
/// struct Team: TableRecord, FetchableRecord { ... }
|
||||
///
|
||||
/// struct PlayerInfo {
|
||||
/// var player: Player
|
||||
/// var team: Team?
|
||||
/// }
|
||||
///
|
||||
/// extension PlayerInfo: FetchableRecord {
|
||||
/// init(row: Row) {
|
||||
/// player = row["player"]
|
||||
/// team = row["team"]
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// extension PlayerInfo {
|
||||
/// /// The request for the player info, given a player id
|
||||
/// static func filter(playerId: Int64) -> some FetchRequest<PlayerInfo> {
|
||||
/// // Build SQL request with SQL interpolation
|
||||
/// let request: SQLRequest<PlayerInfo> = """
|
||||
/// SELECT
|
||||
/// \(columnsOf: Player.self), -- Instead of player.*
|
||||
/// \(columnsOf: Team.self), -- Instead of team.*
|
||||
/// FROM player
|
||||
/// LEFT JOIN team ON team.id = player.teamId
|
||||
/// WHERE player.id = \(playerId)
|
||||
/// """
|
||||
///
|
||||
/// // Returns an adapted request that defines the player and team
|
||||
/// // scopes in the fetched row
|
||||
/// return request.adapted { db in
|
||||
/// let adapters = try splittingRowAdapters(columnCounts: [
|
||||
/// Player.numberOfSelectedColumns(db),
|
||||
/// Team.numberOfSelectedColumns(db),
|
||||
/// ])
|
||||
/// return ScopeAdapter([
|
||||
/// "player": adapters[0],
|
||||
/// "team": adapters[1],
|
||||
/// ])
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Usage
|
||||
/// try dbQueue.read { db in
|
||||
/// if let playerInfo = try PlayerInfo.filter(playerId: 1).fetchOne(db) {
|
||||
/// print(playerInfo.player) // Player(id: 1, name: "Arthur", teamId: 42)
|
||||
/// print(playerInfo.team) // Team(id: 42, name: "Reds")
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter columnCounts: An array of row chunk lengths.
|
||||
/// - returns: An array of row adapters that split a row into as many chunks
|
||||
/// as the number of elements in `columnCounts`, plus one (the row adapter
|
||||
/// for all columns that remain on the right of the last chunk).
|
||||
public func splittingRowAdapters(columnCounts: [Int]) -> [any RowAdapter] {
|
||||
if columnCounts.isEmpty {
|
||||
// Identity adapter
|
||||
return [SuffixRowAdapter(fromIndex: 0)]
|
||||
}
|
||||
|
||||
// [1, 3, 2] -> [0, 1, 4, 6]
|
||||
let columnIndexes = columnCounts.reduce(into: [0]) { (acc, count) in
|
||||
acc.append(acc.last! + count)
|
||||
}
|
||||
|
||||
// [0, 1, 4, 6] -> [(0..<1), (1..<4), (4..<6)]
|
||||
let rangeAdapters = zip(columnIndexes, columnIndexes.suffix(from: 1))
|
||||
.map { RangeRowAdapter($0..<$1) }
|
||||
|
||||
// (6...)
|
||||
let suffixAdapter = SuffixRowAdapter(fromIndex: columnIndexes.last!)
|
||||
|
||||
// [(0..<1), (1..<4), (4..<6), (6...)]
|
||||
return rangeAdapters + [suffixAdapter]
|
||||
}
|
||||
|
||||
/// _LayoutedColumnMapping is a type that supports the RowAdapter protocol.
|
||||
public struct _LayoutedColumnMapping {
|
||||
/// An array of (baseIndex, mappedName) pairs, where baseIndex is the index
|
||||
/// of a column in a base row, and mappedName the mapped name of
|
||||
/// that column.
|
||||
public let _layoutColumns: [(Int, String)]
|
||||
|
||||
/// A cache for layoutIndex(ofColumn:)
|
||||
let lowercaseColumnIndexes: [String: Int] // [mappedColumn: layoutColumnIndex]
|
||||
|
||||
/// Creates a _LayoutedColumnMapping from an array of (baseIndex, mappedName)
|
||||
/// pairs. In each pair:
|
||||
///
|
||||
/// - baseIndex is the index of a column in a base row
|
||||
/// - name is the mapped name of the column
|
||||
///
|
||||
/// For example, the following _LayoutedColumnMapping defines two columns, "foo"
|
||||
/// and "bar", based on the base columns at indexes 1 and 2:
|
||||
///
|
||||
/// _LayoutedColumnMapping(layoutColumns: [(1, "foo"), (2, "bar")])
|
||||
///
|
||||
/// Use it in your custom RowAdapter type:
|
||||
///
|
||||
/// struct FooBarAdapter : RowAdapter {
|
||||
/// func layoutAdapter(layout: _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
/// return _LayoutedColumnMapping(layoutColumns: [(1, "foo"), (2, "bar")])
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // [foo:"foo" bar: "bar"]
|
||||
/// try Row.fetchOne(db, sql: "SELECT NULL, 'foo', 'bar'", adapter: FooBarAdapter())
|
||||
init<S>(layoutColumns: S)
|
||||
where S: Sequence, S.Element == (Int, String)
|
||||
{
|
||||
self._layoutColumns = Array(layoutColumns)
|
||||
self.lowercaseColumnIndexes = Dictionary(
|
||||
layoutColumns
|
||||
.enumerated()
|
||||
.map { ($0.element.1.lowercased(), $0.offset) },
|
||||
uniquingKeysWith: { (left, _) in left }) // keep leftmost indexes
|
||||
}
|
||||
|
||||
func baseColumnIndex(atMappingIndex index: Int) -> Int {
|
||||
_layoutColumns[index].0
|
||||
}
|
||||
|
||||
func columnName(atMappingIndex index: Int) -> String {
|
||||
_layoutColumns[index].1
|
||||
}
|
||||
}
|
||||
|
||||
extension _LayoutedColumnMapping: _LayoutedRowAdapter {
|
||||
/// Returns self.
|
||||
public var _mapping: _LayoutedColumnMapping { self }
|
||||
|
||||
/// Returns the empty dictionary.
|
||||
public var _scopes: [String: any _LayoutedRowAdapter] { [:] }
|
||||
}
|
||||
|
||||
extension _LayoutedColumnMapping: _RowLayout {
|
||||
/// Returns the index of the leftmost column named `name`, in a
|
||||
/// case-insensitive way.
|
||||
public func _layoutIndex(ofColumn name: String) -> Int? {
|
||||
if let index = lowercaseColumnIndexes[name] {
|
||||
return index
|
||||
}
|
||||
return lowercaseColumnIndexes[name.lowercased()]
|
||||
}
|
||||
}
|
||||
|
||||
/// `_LayoutedRowAdapter` is a protocol that supports the `RowAdapter` protocol.
|
||||
///
|
||||
/// GRBD ships with a ready-made type that adopts this protocol:
|
||||
/// `_LayoutedColumnMapping`.
|
||||
public protocol _LayoutedRowAdapter {
|
||||
/// A LayoutedColumnMapping that defines how to map a column name to a
|
||||
/// column in a base row.
|
||||
var _mapping: _LayoutedColumnMapping { get }
|
||||
|
||||
/// The layouted row adapters for each scope.
|
||||
var _scopes: [String: any _LayoutedRowAdapter] { get }
|
||||
}
|
||||
|
||||
/// `_RowLayout` is a protocol that supports the `RowAdapter` protocol. It
|
||||
/// describes the layout of a base row.
|
||||
public protocol _RowLayout {
|
||||
/// An array of (baseIndex, name) pairs, where baseIndex is the index
|
||||
/// of a column in a base row, and name the name of that column.
|
||||
var _layoutColumns: [(Int, String)] { get }
|
||||
|
||||
/// Returns the index of the leftmost column named `name`, in a
|
||||
/// case-insensitive way.
|
||||
func _layoutIndex(ofColumn name: String) -> Int?
|
||||
}
|
||||
|
||||
extension Statement: _RowLayout {
|
||||
public var _layoutColumns: [(Int, String)] {
|
||||
Array(columnNames.enumerated())
|
||||
}
|
||||
|
||||
public func _layoutIndex(ofColumn name: String) -> Int? {
|
||||
index(ofColumn: name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type that helps two incompatible row interfaces working together.
|
||||
///
|
||||
/// Row adapters present database rows in the way expected by the
|
||||
/// row consumers.
|
||||
///
|
||||
/// For example, when a row consumer expects a column named "consumed", but
|
||||
/// the raw row has a column named "produced", the ``ColumnMapping`` row
|
||||
/// adapter comes in handy:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Feeds the "consumed" column from "produced":
|
||||
/// let adapter = ColumnMapping(["consumed": "produced"])
|
||||
/// let sql = "SELECT 'Hello' AS produced"
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
///
|
||||
/// // [consumed:"Hello"]
|
||||
/// print(row)
|
||||
///
|
||||
/// // "Hello"
|
||||
/// print(row["consumed"])
|
||||
/// ```
|
||||
///
|
||||
/// The raw fetched columns are not lost (see ``Row/unadapted``):
|
||||
///
|
||||
/// ```swift
|
||||
/// // ▿ [consumed:"Hello"]
|
||||
/// // unadapted: [produced:"Hello"]
|
||||
/// print(row.debugDescription)
|
||||
///
|
||||
/// // [produced:"Hello"]
|
||||
/// print(row.unadapted)
|
||||
/// ```
|
||||
///
|
||||
/// There are several situations where row adapters are useful. Among them:
|
||||
///
|
||||
/// - Adapters help disambiguate columns with identical names, which may
|
||||
/// happen when you select columns from several tables.
|
||||
/// See ``splittingRowAdapters(columnCounts:)`` for some sample code.
|
||||
///
|
||||
/// - Adapters help when SQLite outputs unexpected column names, which may
|
||||
/// happen with some subqueries. See ``RenameColumnAdapter`` for
|
||||
/// an example.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Splitting a Row into Chunks
|
||||
///
|
||||
/// - ``splittingRowAdapters(columnCounts:)``
|
||||
///
|
||||
/// ### Adding Scopes to an Adapter
|
||||
///
|
||||
/// - ``addingScopes(_:)``
|
||||
///
|
||||
/// ### Built-in Adapters
|
||||
///
|
||||
/// - ``ColumnMapping``
|
||||
/// - ``EmptyRowAdapter``
|
||||
/// - ``RangeRowAdapter``
|
||||
/// - ``RenameColumnAdapter``
|
||||
/// - ``ScopeAdapter``
|
||||
/// - ``SuffixRowAdapter``
|
||||
public protocol RowAdapter {
|
||||
/// You never call this method directly. It is called for you whenever an
|
||||
/// adapter has to be applied.
|
||||
///
|
||||
/// The result is a value that adopts _LayoutedRowAdapter, such as
|
||||
/// _LayoutedColumnMapping.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // An adapter that turns any row to a row that contains a single
|
||||
/// // column named "foo" whose value is the leftmost value of the
|
||||
/// // base row.
|
||||
/// struct FirstColumnAdapter : RowAdapter {
|
||||
/// func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
/// return _LayoutedColumnMapping(layoutColumns: [(0, "foo")])
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // [foo:1]
|
||||
/// try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: FirstColumnAdapter())
|
||||
func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter
|
||||
}
|
||||
|
||||
extension RowAdapter {
|
||||
/// Returns an adapter based on self, with added scopes.
|
||||
///
|
||||
/// If self already defines scopes, the added scopes replace
|
||||
/// eventual existing scopes with the same name.
|
||||
///
|
||||
/// - parameter scopes: A dictionary that maps scope names to
|
||||
/// row adapters.
|
||||
public func addingScopes(_ scopes: [String: any RowAdapter]) -> any RowAdapter {
|
||||
if scopes.isEmpty {
|
||||
return self
|
||||
} else {
|
||||
return ScopeAdapter(base: self, scopes: scopes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RowAdapter {
|
||||
func baseColumnIndex(atIndex index: Int, layout: some _RowLayout) throws -> Int {
|
||||
try _layoutedAdapter(from: layout)._mapping.baseColumnIndex(atMappingIndex: index)
|
||||
}
|
||||
}
|
||||
|
||||
/// `EmptyRowAdapter` is a row adapter that hides all columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapter = EmptyRowAdapter()
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c"
|
||||
///
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
/// row.isEmpty // true
|
||||
/// ```
|
||||
///
|
||||
/// This limit adapter may turn out useful in some narrow use cases. You'll
|
||||
/// be happy to find it when you need it.
|
||||
public struct EmptyRowAdapter: RowAdapter, Sendable {
|
||||
/// Creates an `EmptyRowAdapter`.
|
||||
public init() { }
|
||||
|
||||
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
_LayoutedColumnMapping(layoutColumns: [])
|
||||
}
|
||||
}
|
||||
|
||||
/// `ColumnMapping` is a row adapter that maps column names.
|
||||
///
|
||||
/// Build a `ColumnMapping` with a dictionary whose keys
|
||||
/// are adapted column names, and values the column names in the base row:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Feeds "newA" from "a", and "newB" from "b":
|
||||
/// let adapter = ColumnMapping(["newA": "a", "newB": "b"])
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c"
|
||||
///
|
||||
/// // [newA:0, newB:1]
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
/// ```
|
||||
///
|
||||
/// Note that columns that are not present in the dictionary are not present
|
||||
/// in the resulting adapted row.
|
||||
public struct ColumnMapping: RowAdapter, Sendable {
|
||||
/// A dictionary from mapped column names to column names in a base row.
|
||||
let mapping: [String: String]
|
||||
|
||||
/// Creates a `ColumnMapping` with a dictionary from mapped column names
|
||||
/// to column names in a base row.
|
||||
public init(_ mapping: [String: String]) {
|
||||
self.mapping = mapping
|
||||
}
|
||||
|
||||
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
let layoutColumns = try mapping
|
||||
.map { (mappedColumn, baseColumn) -> (Int, String) in
|
||||
guard let index = layout._layoutIndex(ofColumn: baseColumn) else {
|
||||
let columnNames = layout._layoutColumns.map { $0.1 }
|
||||
throw DatabaseError(
|
||||
resultCode: .SQLITE_MISUSE,
|
||||
message: """
|
||||
Mapping references missing column \(baseColumn). \
|
||||
Valid column names are: \(columnNames.joined(separator: ", ")).
|
||||
""")
|
||||
}
|
||||
let baseIndex = layout._layoutColumns[index].0
|
||||
return (baseIndex, mappedColumn)
|
||||
}
|
||||
.sorted { $0.0 < $1.0 } // preserve ordering of base columns
|
||||
return _LayoutedColumnMapping(layoutColumns: layoutColumns)
|
||||
}
|
||||
}
|
||||
|
||||
/// `SuffixRowAdapter` hides the leftmost columns in a row.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapter = SuffixRowAdapter(fromIndex: 2)
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
|
||||
///
|
||||
/// // [c:2, d: 3]
|
||||
/// try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
/// ```
|
||||
public struct SuffixRowAdapter: RowAdapter, Sendable {
|
||||
/// The suffix index
|
||||
let index: Int
|
||||
|
||||
/// Creates a SuffixRowAdapter that hides all columns before the
|
||||
/// provided index.
|
||||
///
|
||||
/// If index is 0, the layout row is identical to the base row.
|
||||
public init(fromIndex index: Int) {
|
||||
GRDBPrecondition(index >= 0, "Negative column index is out of range")
|
||||
self.index = index
|
||||
}
|
||||
|
||||
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
_LayoutedColumnMapping(layoutColumns: layout._layoutColumns.suffix(from: index))
|
||||
}
|
||||
}
|
||||
|
||||
/// `RangeRowAdapter` is a row adapter that only exposes a range of columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapter = RangeRowAdapter(1..<3)
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
|
||||
///
|
||||
/// // [b:1 c:2]
|
||||
/// try Row.fetchOne(db, sql: sql, adapter: adapter)
|
||||
/// ```
|
||||
public struct RangeRowAdapter: RowAdapter, Sendable {
|
||||
/// The range
|
||||
let range: CountableRange<Int>
|
||||
|
||||
/// Creates a RangeRowAdapter that only exposes a range of columns.
|
||||
public init(_ range: CountableRange<Int>) {
|
||||
GRDBPrecondition(range.lowerBound >= 0, "Negative column index is out of range")
|
||||
self.range = range
|
||||
}
|
||||
|
||||
/// Creates a RangeRowAdapter that only exposes a range of columns.
|
||||
public init(_ range: CountableClosedRange<Int>) {
|
||||
GRDBPrecondition(range.lowerBound >= 0, "Negative column index is out of range")
|
||||
self.range = range.lowerBound..<(range.upperBound + 1)
|
||||
}
|
||||
|
||||
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
_LayoutedColumnMapping(layoutColumns: layout._layoutColumns[range])
|
||||
}
|
||||
}
|
||||
|
||||
/// `ScopeAdapter` is a row adapter that defines row scopes.
|
||||
///
|
||||
/// `ScopeAdapter` does not change the columns and values of the fetched
|
||||
/// row. Instead, it defines *scopes* based on other adapter, which you
|
||||
/// access through the ``Row/scopes`` property of the fetched rows.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapter = ScopeAdapter([
|
||||
/// "left": RangeRowAdapter(0..<2),
|
||||
/// "right": RangeRowAdapter(2..<4)])
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
|
||||
///
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
///
|
||||
/// row // [a:0 b:1 c:2 d:3]
|
||||
/// row.scopes["left"] // [a:0 b:1]
|
||||
/// row.scopes["right"] // [c:2 d:3]
|
||||
/// row.scopes["missing"] // nil
|
||||
/// ```
|
||||
///
|
||||
/// Scopes can be nested:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapter = ScopeAdapter([
|
||||
/// "left": ScopeAdapter([
|
||||
/// "left": RangeRowAdapter(0..<1),
|
||||
/// "right": RangeRowAdapter(1..<2)]),
|
||||
/// "right": ScopeAdapter([
|
||||
/// "left": RangeRowAdapter(2..<3),
|
||||
/// "right": RangeRowAdapter(3..<4)])
|
||||
/// ])
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
///
|
||||
/// let leftRow = row.scopes["left"]!
|
||||
/// leftRow.scopes["left"] // [a:0]
|
||||
/// leftRow.scopes["right"] // [b:1]
|
||||
///
|
||||
/// let rightRow = row.scopes["right"]!
|
||||
/// rightRow.scopes["left"] // [c:2]
|
||||
/// rightRow.scopes["right"] // [d:3]
|
||||
/// ```
|
||||
///
|
||||
/// Any adapter can be extended with scopes, with
|
||||
/// ``RowAdapter/addingScopes(_:)``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let baseAdapter = RangeRowAdapter(0..<2)
|
||||
/// let adapter = baseAdapter.addingScopes([
|
||||
/// "remainder": SuffixRowAdapter(fromIndex: 2)
|
||||
/// ])
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d"
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
///
|
||||
/// row // [a:0 b:1]
|
||||
/// row.scopes["remainder"] // [c:2 d:3]
|
||||
/// ```
|
||||
public struct ScopeAdapter: RowAdapter {
|
||||
|
||||
/// The base adapter
|
||||
let base: any RowAdapter
|
||||
|
||||
/// The scope adapters
|
||||
let scopes: [String: any RowAdapter]
|
||||
|
||||
/// Creates an adapter that preserves row contents and add scoped rows.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let adapter = ScopeAdapter(["suffix": SuffixRowAdapter(fromIndex: 1)])
|
||||
/// let row = try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: adapter)!
|
||||
/// row // [1, 2, 3]
|
||||
/// row.scopes["suffix"] // [2, 3]
|
||||
///
|
||||
/// - parameter scopes: A dictionary that maps scope names to
|
||||
/// row adapters.
|
||||
public init(_ scopes: [String: any RowAdapter]) {
|
||||
// Use SuffixRowAdapter(fromIndex: 0) as the identity adapter
|
||||
self.init(base: SuffixRowAdapter(fromIndex: 0), scopes: scopes)
|
||||
}
|
||||
|
||||
/// Creates an adapter based on the base adapter, and add scoped rows.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let baseAdapter = RangeRowAdapter(0..<1)
|
||||
/// let adapter = ScopeAdapter(base: baseAdapter, scopes: ["suffix": SuffixRowAdapter(fromIndex: 1)])
|
||||
/// let row = try Row.fetchOne(db, sql: "SELECT 1, 2, 3", adapter: adapter)!
|
||||
/// row // [1]
|
||||
/// row.scopes["initial"] // [2, 3]
|
||||
///
|
||||
/// If the base adapter already defines scopes, the given scopes replace
|
||||
/// eventual existing scopes with the same name.
|
||||
///
|
||||
/// This initializer is equivalent to `baseAdapter.addingScopes(scopes)`.
|
||||
///
|
||||
/// - parameter base: A dictionary that maps scope names to
|
||||
/// row adapters.
|
||||
/// - parameter scopes: A dictionary that maps scope names to
|
||||
/// row adapters.
|
||||
public init(base: some RowAdapter, scopes: [String: any RowAdapter]) {
|
||||
self.base = base
|
||||
self.scopes = scopes
|
||||
}
|
||||
|
||||
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
let layoutedAdapter = try base._layoutedAdapter(from: layout)
|
||||
var layoutedScopes = layoutedAdapter._scopes
|
||||
for (name, adapter) in scopes {
|
||||
try layoutedScopes[name] = adapter._layoutedAdapter(from: layout)
|
||||
}
|
||||
return LayoutedScopeAdapter(
|
||||
_mapping: layoutedAdapter._mapping,
|
||||
_scopes: layoutedScopes)
|
||||
}
|
||||
}
|
||||
|
||||
/// The `_LayoutedRowAdapter` for `ScopeAdapter`
|
||||
struct LayoutedScopeAdapter: _LayoutedRowAdapter {
|
||||
let _mapping: _LayoutedColumnMapping
|
||||
let _scopes: [String: any _LayoutedRowAdapter]
|
||||
}
|
||||
|
||||
struct ChainedAdapter: RowAdapter {
|
||||
let first: any RowAdapter
|
||||
let second: any RowAdapter
|
||||
|
||||
func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
try second._layoutedAdapter(from: first._layoutedAdapter(from: layout)._mapping)
|
||||
}
|
||||
}
|
||||
|
||||
/// `RenameColumnAdapter` is a row adapter that renames columns.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let adapter = RenameColumnAdapter { column in column + "rrr" }
|
||||
/// let sql = "SELECT 0 AS a, 1 AS b, 2 AS c"
|
||||
///
|
||||
/// // [arrr:0, brrr:1, crrr:2]
|
||||
/// let row = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
/// ```
|
||||
///
|
||||
/// This adapter is useful when subqueries contain duplicated column names:
|
||||
///
|
||||
/// ```swift
|
||||
/// let sql = "SELECT * FROM (SELECT 1 AS id, 2 AS id)"
|
||||
///
|
||||
/// // Prints ["id", "id:1"]
|
||||
/// // Note the "id:1" column, generated by SQLite.
|
||||
/// let row = try Row.fetchOne(db, sql: sql)!
|
||||
/// print(Array(row.columnNames))
|
||||
///
|
||||
/// // Drop the `:...` suffix, and prints ["id", "id"]
|
||||
/// let adapter = RenameColumnAdapter { String($0.prefix(while: { $0 != ":" })) }
|
||||
/// let adaptedRow = try Row.fetchOne(db, sql: sql, adapter: adapter)!
|
||||
/// print(Array(adaptedRow.columnNames))
|
||||
/// ```
|
||||
public struct RenameColumnAdapter: RowAdapter {
|
||||
let transform: (String) -> String
|
||||
|
||||
/// Creates a `RenameColumnAdapter` adapter that renames columns according to the
|
||||
/// provided transform function.
|
||||
public init(_ transform: @escaping (String) -> String) {
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
public func _layoutedAdapter(from layout: some _RowLayout) throws -> any _LayoutedRowAdapter {
|
||||
let layoutColumns = layout._layoutColumns.map { (index, column) in (index, transform(column)) }
|
||||
return _LayoutedColumnMapping(layoutColumns: layoutColumns)
|
||||
}
|
||||
}
|
||||
|
||||
extension Row {
|
||||
/// Creates a row from a base row and a statement adapter
|
||||
convenience init(base: Row, adapter: some _LayoutedRowAdapter) {
|
||||
self.init(impl: AdaptedRowImpl(base: base, adapter: adapter))
|
||||
}
|
||||
|
||||
/// Returns self if adapter is nil
|
||||
func adapted(with adapter: (any RowAdapter)?, layout: some _RowLayout) throws -> Row {
|
||||
guard let adapter else {
|
||||
return self
|
||||
}
|
||||
return try Row(base: self, adapter: adapter._layoutedAdapter(from: layout))
|
||||
}
|
||||
}
|
||||
|
||||
struct AdaptedRowImpl: RowImpl {
|
||||
let base: Row
|
||||
let adapter: any _LayoutedRowAdapter
|
||||
let mapping: _LayoutedColumnMapping
|
||||
|
||||
init(base: Row, adapter: some _LayoutedRowAdapter) {
|
||||
self.base = base
|
||||
self.adapter = adapter
|
||||
self.mapping = adapter._mapping
|
||||
}
|
||||
|
||||
var count: Int { mapping._layoutColumns.count }
|
||||
|
||||
var isFetched: Bool { base.isFetched }
|
||||
|
||||
func scopes(prefetchedRows: Row.PrefetchedRowsView) -> Row.ScopesView {
|
||||
Row.ScopesView(row: base, scopes: adapter._scopes, prefetchedRows: prefetchedRows)
|
||||
}
|
||||
|
||||
func hasNull(atUncheckedIndex index: Int) -> Bool {
|
||||
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
|
||||
return base.impl.hasNull(atUncheckedIndex: mappedIndex)
|
||||
}
|
||||
|
||||
func databaseValue(atUncheckedIndex index: Int) -> DatabaseValue {
|
||||
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
|
||||
return base.impl.databaseValue(atUncheckedIndex: mappedIndex)
|
||||
}
|
||||
|
||||
func fastDecode<Value: DatabaseValueConvertible & StatementColumnConvertible>(
|
||||
_ type: Value.Type,
|
||||
atUncheckedIndex index: Int)
|
||||
throws -> Value
|
||||
{
|
||||
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
|
||||
return try Value.fastDecode(fromRow: base, atUncheckedIndex: mappedIndex)
|
||||
}
|
||||
|
||||
func withUnsafeData<T>(atUncheckedIndex index: Int, _ body: (Data?) throws -> T) throws -> T {
|
||||
let mappedIndex = mapping.baseColumnIndex(atMappingIndex: index)
|
||||
return try base.impl.withUnsafeData(atUncheckedIndex: mappedIndex, body)
|
||||
}
|
||||
|
||||
func columnName(atUncheckedIndex index: Int) -> String {
|
||||
mapping.columnName(atMappingIndex: index)
|
||||
}
|
||||
|
||||
func index(forColumn name: String) -> Int? {
|
||||
mapping._layoutIndex(ofColumn: name)
|
||||
}
|
||||
|
||||
func copiedRow(_ row: Row) -> Row {
|
||||
Row(base: base.copy(), adapter: adapter)
|
||||
}
|
||||
|
||||
func unscopedRow(_ row: Row) -> Row {
|
||||
assert(adapter._mapping._scopes.isEmpty)
|
||||
return Row(base: base, adapter: adapter._mapping)
|
||||
}
|
||||
|
||||
func unadaptedRow(_ row: Row) -> Row {
|
||||
base.unadapted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/// A key that is used to decode a value in a row
|
||||
@usableFromInline
|
||||
enum RowKey: Hashable, Sendable {
|
||||
/// A column name
|
||||
case columnName(String)
|
||||
|
||||
/// A column index
|
||||
case columnIndex(Int)
|
||||
|
||||
/// A scope
|
||||
case scope(String)
|
||||
|
||||
/// A key of prefetched rows
|
||||
case prefetchKey(String)
|
||||
}
|
||||
|
||||
/// A decoding error
|
||||
@usableFromInline
|
||||
enum RowDecodingError: Error {
|
||||
@usableFromInline
|
||||
struct Context: CustomDebugStringConvertible, Sendable {
|
||||
/// A description of what went wrong, for debugging purposes.
|
||||
@usableFromInline
|
||||
let debugDescription: String
|
||||
|
||||
let rowImpl: ArrayRowImpl // Sendable
|
||||
|
||||
/// The row that could not be decoded
|
||||
var row: Row { Row(impl: rowImpl) }
|
||||
|
||||
/// Nil for RowDecodingError.keyNotFound, in order to avoid redundancy
|
||||
let key: RowKey?
|
||||
|
||||
/// The SQL query
|
||||
let sql: String?
|
||||
|
||||
/// The SQL query arguments
|
||||
let statementArguments: StatementArguments?
|
||||
|
||||
init(decodingContext: RowDecodingContext, debugDescription: String) {
|
||||
self.debugDescription = debugDescription
|
||||
self.rowImpl = ArrayRowImpl(columns: decodingContext.row)
|
||||
self.key = decodingContext.key
|
||||
self.sql = decodingContext.sql
|
||||
self.statementArguments = decodingContext.statementArguments
|
||||
}
|
||||
}
|
||||
|
||||
case keyNotFound(RowKey, Context)
|
||||
case valueMismatch(Any.Type, Context)
|
||||
|
||||
var context: Context {
|
||||
switch self {
|
||||
case .keyNotFound(_, let context),
|
||||
.valueMismatch(_, let context):
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method that builds the
|
||||
/// `could not decode <Type> from database value <value>` error message.
|
||||
static func valueMismatch(
|
||||
_ type: Any.Type,
|
||||
context: RowDecodingContext,
|
||||
databaseValue: DatabaseValue)
|
||||
-> Self
|
||||
{
|
||||
valueMismatch(
|
||||
type,
|
||||
RowDecodingError.Context(decodingContext: context, debugDescription: """
|
||||
could not decode \(type) from database value \(databaseValue)
|
||||
"""))
|
||||
}
|
||||
|
||||
/// Convenience method that builds the
|
||||
/// `could not decode <Type> from database value <value>` error message.
|
||||
@usableFromInline
|
||||
static func valueMismatch(
|
||||
_ type: Any.Type,
|
||||
sqliteStatement: SQLiteStatement,
|
||||
index: CInt,
|
||||
context: RowDecodingContext)
|
||||
-> Self
|
||||
{
|
||||
valueMismatch(
|
||||
type,
|
||||
context: context,
|
||||
databaseValue: DatabaseValue(sqliteStatement: sqliteStatement, index: index))
|
||||
}
|
||||
|
||||
/// Convenience method that builds the
|
||||
/// `could not decode <Type> from database value <value>` error message.
|
||||
static func valueMismatch(
|
||||
_ type: Any.Type,
|
||||
statement: Statement,
|
||||
index: Int)
|
||||
-> Self
|
||||
{
|
||||
valueMismatch(
|
||||
type,
|
||||
context: RowDecodingContext(statement: statement, index: index),
|
||||
databaseValue: DatabaseValue(sqliteStatement: statement.sqliteStatement, index: CInt(index)))
|
||||
}
|
||||
|
||||
/// Convenience method that builds the `column not found: <column>`
|
||||
/// error message.
|
||||
@usableFromInline
|
||||
static func columnNotFound(_ columnName: String, context: RowDecodingContext) -> Self {
|
||||
keyNotFound(
|
||||
.columnName(columnName),
|
||||
RowDecodingError.Context(decodingContext: context, debugDescription: """
|
||||
column not found: \(String(reflecting: columnName))
|
||||
"""))
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct RowDecodingContext {
|
||||
/// The row that is decoded
|
||||
let row: Row
|
||||
|
||||
let key: RowKey?
|
||||
|
||||
/// The SQL query
|
||||
let sql: String?
|
||||
|
||||
/// The SQL query arguments
|
||||
let statementArguments: StatementArguments?
|
||||
|
||||
@usableFromInline
|
||||
init(row: Row, key: RowKey? = nil) {
|
||||
if let statement = row.statement {
|
||||
self.key = key
|
||||
self.row = row.copy()
|
||||
self.sql = statement.sql
|
||||
self.statementArguments = statement.arguments
|
||||
} else if let sqliteStatement = row.sqliteStatement {
|
||||
self.key = key
|
||||
self.row = row.copy()
|
||||
self.sql = String(cString: sqlite3_sql(sqliteStatement)).trimmedSQLStatement
|
||||
self.statementArguments = nil // Can't rebuild them
|
||||
} else {
|
||||
self.key = key
|
||||
self.row = row.copy()
|
||||
self.sql = nil
|
||||
self.statementArguments = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience initializer
|
||||
@usableFromInline
|
||||
init(statement: Statement, index: Int) {
|
||||
self.key = .columnIndex(index)
|
||||
self.row = Row(copiedFromSQLiteStatement: statement.sqliteStatement, statement: statement)
|
||||
self.sql = statement.sql
|
||||
self.statementArguments = statement.arguments
|
||||
}
|
||||
}
|
||||
|
||||
extension RowDecodingError: CustomStringConvertible {
|
||||
@usableFromInline
|
||||
var description: String {
|
||||
let context = self.context
|
||||
let row = context.row
|
||||
var chunks: [String] = []
|
||||
|
||||
if let key = context.key {
|
||||
switch key {
|
||||
case let .columnIndex(columnIndex):
|
||||
let rowIndex = row.index(row.startIndex, offsetBy: columnIndex)
|
||||
let columnName = row.columnNames[rowIndex]
|
||||
chunks.append("column: \(String(reflecting: columnName))")
|
||||
chunks.append("column index: \(columnIndex)")
|
||||
|
||||
case let .columnName(columnName):
|
||||
if let columnIndex = row.index(forColumn: columnName) {
|
||||
chunks.append("column: \(String(reflecting: columnName))")
|
||||
chunks.append("column index: \(columnIndex)")
|
||||
} else {
|
||||
// column name is already mentioned in context.debugDescription
|
||||
}
|
||||
|
||||
case .prefetchKey:
|
||||
// key is already mentioned in context.debugDescription
|
||||
break
|
||||
|
||||
case .scope:
|
||||
// scope is already mentioned in context.debugDescription
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
chunks.append("row: \(row.description)")
|
||||
|
||||
if let sql = context.sql {
|
||||
chunks.append("sql: `\(sql)`")
|
||||
}
|
||||
|
||||
if let statementArguments = context.statementArguments {
|
||||
chunks.append("arguments: \(statementArguments)")
|
||||
}
|
||||
|
||||
return "\(context.debugDescription) - \(chunks.joined(separator: ", "))"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
/// An SQL literal.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.write { db in
|
||||
/// let name: String = "O'Brien"
|
||||
/// let id: Int64 = 42
|
||||
/// let query: SQL = "UPDATE player SET name = \(name) WHERE id = \(id)"
|
||||
///
|
||||
/// // UPDATE player SET name = 'O''Brien' WHERE id = 42
|
||||
/// try db.execute(literal: query)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating an SQL Literal from a Literal Value
|
||||
///
|
||||
/// - ``init(stringLiteral:)``
|
||||
/// - ``init(unicodeScalarLiteral:)-7p0al``
|
||||
/// - ``init(extendedGraphemeClusterLiteral:)-1oham``
|
||||
///
|
||||
/// ### Creating an SQL Literal from an Interpolation
|
||||
///
|
||||
/// - ``init(stringInterpolation:)``
|
||||
/// - ``SQLInterpolation``
|
||||
///
|
||||
/// ### Creating an SQL Literal from an SQL String
|
||||
///
|
||||
/// - ``init(sql:arguments:)``
|
||||
///
|
||||
/// ### Creating an SQL Literal from an SQL Expression
|
||||
///
|
||||
/// - ``init(_:)``
|
||||
///
|
||||
/// ### Concatenating SQL Literals
|
||||
///
|
||||
/// - ``append(literal:)``
|
||||
/// - ``append(sql:arguments:)``
|
||||
public struct SQL {
|
||||
/// `SQL.Element` is a component of an `SQL` literal.
|
||||
///
|
||||
/// Elements can be qualified with table aliases, and this is how `SQL`
|
||||
/// blends well in the query interface. See below how the `createdAt` column
|
||||
/// is qualified with the `player` table in the generated SQL, in order to
|
||||
/// avoid any conflict with the `team.createdAt` column:
|
||||
///
|
||||
/// func date(_ value: SQLSpecificExpressible) -> SQLExpression {
|
||||
/// // An SQL literal made of three elements:
|
||||
/// // - "DATE(" raw sql string
|
||||
/// // - expression
|
||||
/// // - ")" raw sql string
|
||||
/// SQL("DATE(\(value))").sqlExpression
|
||||
/// }
|
||||
///
|
||||
/// // SELECT player.*, team.*
|
||||
/// // FROM player
|
||||
/// // JOIN team ON team.id = player.teamId
|
||||
/// // WHERE DATE(player.createdAt) = '2022-08-17'
|
||||
/// let request = Player
|
||||
/// .filter(date(Column("createdAt")) == "2022-08-17")
|
||||
/// .including(required: Player.team)
|
||||
enum Element {
|
||||
/// A raw SQL literal with eventual arguments.
|
||||
case sql(String, StatementArguments = StatementArguments())
|
||||
|
||||
/// A subquery.
|
||||
case subquery(SQLSubquery)
|
||||
|
||||
/// An expression.
|
||||
case expression(SQLExpression)
|
||||
|
||||
/// A selection.
|
||||
case selection(SQLSelection)
|
||||
|
||||
/// An ordering.
|
||||
case ordering(SQLOrdering)
|
||||
|
||||
var isEmpty: Bool {
|
||||
switch self {
|
||||
case let .sql(sql, _):
|
||||
return sql.isEmpty
|
||||
default:
|
||||
// Subqueries, expressions, selections and orderings are
|
||||
// assumed to be non-empty.
|
||||
//
|
||||
// Nothing prevents the user from creating an ill-formed empty
|
||||
// expression, but we don't care about such misuse:
|
||||
//
|
||||
// // An ill-formed empty expression
|
||||
// let expression = SQL("").sqlExpression
|
||||
//
|
||||
// let sql: SQL = "\(expression)"
|
||||
// sql.isEmpty // false, deal with it ¯\_(ツ)_/¯
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func sql(_ context: SQLGenerationContext) throws -> String {
|
||||
switch self {
|
||||
case let .sql(sql, arguments):
|
||||
if context.append(arguments: arguments) == false {
|
||||
// We don't know how to look for `?` in sql and
|
||||
// replace them with literals.
|
||||
fatalError("Not implemented: turning an SQL parameter into an SQL literal value")
|
||||
}
|
||||
return sql
|
||||
case let .subquery(subquery):
|
||||
return try subquery.sql(context)
|
||||
case let .expression(expression):
|
||||
return try expression.sql(context)
|
||||
case let .selection(selection):
|
||||
return try selection.sql(context)
|
||||
case let .ordering(ordering):
|
||||
return try ordering.sql(context)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func qualified(with alias: TableAlias) -> Element {
|
||||
switch self {
|
||||
case .sql:
|
||||
// A raw SQL string can't be qualified with a table alias,
|
||||
// because we can't parse it.
|
||||
return self
|
||||
case .subquery:
|
||||
// Subqueries don't need table alias
|
||||
return self
|
||||
case let .expression(expression):
|
||||
return .expression(expression.qualified(with: alias))
|
||||
case let .selection(selection):
|
||||
return .selection(selection.qualified(with: alias))
|
||||
case let .ordering(ordering):
|
||||
return .ordering(ordering.qualified(with: alias))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var elements: [Element]
|
||||
|
||||
init(elements: [Element]) {
|
||||
self.elements = elements
|
||||
}
|
||||
|
||||
/// Creates an `SQL` literal from a plain SQL string, and
|
||||
/// eventual arguments.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let query = SQL(
|
||||
/// sql: "UPDATE player SET name = ? WHERE id = ?",
|
||||
/// arguments: [name, id])
|
||||
/// ```
|
||||
public init(sql: String, arguments: StatementArguments = StatementArguments()) {
|
||||
self.init(elements: [.sql(sql, arguments)])
|
||||
}
|
||||
|
||||
/// Creates an `SQL` literal from an SQL expression.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let columnLiteral = SQL(Column("username"))
|
||||
/// let suffixLiteral = SQL("@example.com".databaseValue)
|
||||
/// let emailLiteral = [columnLiteral, suffixLiteral].joined(separator: " || ")
|
||||
/// let request = User.select(emailLiteral.sqlExpression)
|
||||
/// let emails = try String.fetchAll(db, request)
|
||||
/// ```
|
||||
public init(_ expression: some SQLSpecificExpressible) {
|
||||
self.init(elements: [.expression(expression.sqlExpression)])
|
||||
}
|
||||
|
||||
/// Returns true if this literal generates an empty SQL string
|
||||
public var isEmpty: Bool {
|
||||
elements.allSatisfy(\.isEmpty)
|
||||
}
|
||||
|
||||
/// Turn a `SQL` literal into raw SQL and arguments.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: A tuple made of a raw SQL string, and statement arguments.
|
||||
public func build(_ db: Database) throws -> (sql: String, arguments: StatementArguments) {
|
||||
let context = SQLGenerationContext(db)
|
||||
let sql = try self.sql(context)
|
||||
return (sql: sql, arguments: context.arguments)
|
||||
}
|
||||
|
||||
/// Returns the literal SQL string given an SQL generation context.
|
||||
func sql(_ context: SQLGenerationContext) throws -> String {
|
||||
try elements.map { try $0.sql(context) }.joined()
|
||||
}
|
||||
|
||||
func qualified(with alias: TableAlias) -> SQL {
|
||||
SQL(elements: elements.map { $0.qualified(with: alias) })
|
||||
}
|
||||
}
|
||||
|
||||
extension SQL {
|
||||
/// Returns the `SQL` literal produced by the concatenation of two literals.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let name = "O'Brien"
|
||||
/// let selection: SQL = "SELECT * FROM player "
|
||||
/// let condition: SQL = "WHERE name = \(name)"
|
||||
/// let query = selection + condition
|
||||
/// ```
|
||||
public static func + (lhs: SQL, rhs: SQL) -> SQL {
|
||||
var result = lhs
|
||||
result += rhs
|
||||
return result
|
||||
}
|
||||
|
||||
/// Appends an `SQL` literal to the receiver.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let name = "O'Brien"
|
||||
/// var query: SQL = "SELECT * FROM player "
|
||||
/// query += "WHERE name = \(name)"
|
||||
/// ```
|
||||
public static func += (lhs: inout SQL, rhs: SQL) {
|
||||
lhs.elements += rhs.elements
|
||||
}
|
||||
|
||||
/// Appends an `SQL` literal to the receiver.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let name = "O'Brien"
|
||||
/// var query: SQL = "SELECT * FROM player "
|
||||
/// query.append(literal: "WHERE name = \(name)")
|
||||
/// ```
|
||||
public mutating func append(literal sqlLiteral: SQL) {
|
||||
self += sqlLiteral
|
||||
}
|
||||
|
||||
/// Appends a plain SQL string to the receiver, and eventual arguments.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let name = "O'Brien"
|
||||
/// var query: SQL = "SELECT * FROM player "
|
||||
/// query.append(sql: "WHERE name = ?", arguments: [name])
|
||||
/// ```
|
||||
public mutating func append(sql: String, arguments: StatementArguments = StatementArguments()) {
|
||||
self += SQL(sql: sql, arguments: arguments)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQL: SQLSpecificExpressible {
|
||||
/// Creates a literal SQL expression.
|
||||
///
|
||||
/// Use this property when you need an explicit `SQLExpression`.
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// func date(_ value: some SQLExpressible) -> SQLExpression {
|
||||
/// SQL("DATE(\(value))").sqlExpression
|
||||
/// }
|
||||
///
|
||||
/// // SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23'
|
||||
/// let createdAt = Column("createdAt")
|
||||
/// let request = Player.filter(date(createdAt) == "2020-01-23")
|
||||
/// ```
|
||||
public var sqlExpression: SQLExpression {
|
||||
.literal(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQL: SQLSelectable {
|
||||
/// Creates a literal SQL result column.
|
||||
///
|
||||
/// Use this property when you need an explicit `SQLSelection`. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // SELECT firstName AS givenName, lastName AS familyName FROM player
|
||||
/// let selection = SQL("firstName AS givenName, lastName AS familyName").sqlSelection
|
||||
/// let request = Player.select(selection)
|
||||
/// ```
|
||||
public var sqlSelection: SQLSelection {
|
||||
.literal(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQL: SQLOrderingTerm {
|
||||
/// Creates a literal SQL ordering term.
|
||||
///
|
||||
/// Use this property when you need an explicit `SQLOrdering`. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // SELECT * FROM player ORDER BY name DESC
|
||||
/// let ordering = SQL("name DESC").sqlOrdering
|
||||
/// let request = Player.order(ordering)
|
||||
/// ```
|
||||
///
|
||||
/// An ordering term is only valid if it represent a single ordering terms.
|
||||
///
|
||||
/// ```swift
|
||||
/// // Valid
|
||||
/// SQL("score DESC").sqlOrdering
|
||||
/// SQL("name").sqlOrdering
|
||||
///
|
||||
/// // Invalid
|
||||
/// SQL("score DESC, name").sqlOrdering
|
||||
/// ```
|
||||
public var sqlOrdering: SQLOrdering {
|
||||
.literal(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Sequence where Element == SQL {
|
||||
/// Returns the concatenated `SQL` literal of this sequence of literals,
|
||||
/// inserting the given raw SQL separator between each element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```
|
||||
/// let components: [SQL] = [
|
||||
/// "UPDATE player",
|
||||
/// "SET name = \(name)",
|
||||
/// "WHERE id = \(id)"
|
||||
/// ]
|
||||
/// let query = components.joined(separator: " ")
|
||||
/// ```
|
||||
///
|
||||
/// - Note: The separator is a raw SQL string, not an ``SQL`` literal.
|
||||
public func joined(separator: String = "") -> SQL {
|
||||
if separator.isEmpty {
|
||||
return SQL(elements: flatMap(\.elements))
|
||||
} else {
|
||||
return SQL(elements: Array(map(\.elements).joined(separator: CollectionOfOne(.sql(separator)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Collection where Element == SQL {
|
||||
/// Returns the concatenated `SQL` literal of this collection of literals,
|
||||
/// inserting the given raw SQL separator between each element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// let components: [SQL] = [
|
||||
/// "UPDATE player",
|
||||
/// "SET name = \(name)",
|
||||
/// "WHERE id = \(id)"
|
||||
/// ]
|
||||
/// let query = components.joined(separator: " ")
|
||||
/// ```
|
||||
///
|
||||
/// - Note: The separator is a raw SQL string, not an ``SQL`` literal.
|
||||
public func joined(separator: String = "") -> SQL {
|
||||
if separator.isEmpty {
|
||||
return SQL(elements: flatMap(\.elements))
|
||||
} else {
|
||||
return SQL(elements: Array(map(\.elements).joined(separator: CollectionOfOne(.sql(separator)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByStringInterpolation
|
||||
|
||||
extension SQL: ExpressibleByStringInterpolation {
|
||||
public init(unicodeScalarLiteral: String) {
|
||||
self.init(sql: unicodeScalarLiteral, arguments: [])
|
||||
}
|
||||
|
||||
public init(extendedGraphemeClusterLiteral: String) {
|
||||
self.init(sql: extendedGraphemeClusterLiteral, arguments: [])
|
||||
}
|
||||
|
||||
/// Creates an `SQL` literal from the given literal SQL string.
|
||||
public init(stringLiteral: String) {
|
||||
self.init(sql: stringLiteral, arguments: [])
|
||||
}
|
||||
|
||||
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
|
||||
self.init(elements: sqlInterpolation.elements)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
public struct SQLInterpolation: StringInterpolationProtocol {
|
||||
var elements: [SQL.Element]
|
||||
|
||||
public init(literalCapacity: Int, interpolationCount: Int) {
|
||||
elements = []
|
||||
elements.reserveCapacity(interpolationCount + 1)
|
||||
}
|
||||
|
||||
public mutating func appendLiteral(_ sql: String) {
|
||||
if sql.isEmpty { return }
|
||||
elements.append(.sql(sql))
|
||||
}
|
||||
|
||||
/// Appends a raw SQL snippet, with eventual arguments.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// "SELECT * FROM \(sql: "player")"
|
||||
/// "SELECT * FROM player WHERE \(sql: "name = ?", arguments: ["O'Brien"])"
|
||||
public mutating func appendInterpolation(sql: String, arguments: StatementArguments = StatementArguments()) {
|
||||
elements.append(.sql(sql, arguments))
|
||||
}
|
||||
|
||||
/// Appends a raw `SQL` literal.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// "SELECT * FROM \(SQL("player"))"
|
||||
/// "SELECT * FROM player WHERE \(SQL("name = \("O'Brien")"))"
|
||||
public mutating func appendInterpolation(_ sqlLiteral: SQL) {
|
||||
elements.append(contentsOf: sqlLiteral.elements)
|
||||
}
|
||||
|
||||
/// Appends a String expression.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// "SELECT * FROM player WHERE name = \("O'Brien")"
|
||||
public mutating func appendInterpolation<S: StringProtocol>(_ string: S) {
|
||||
elements.append(.expression(String(string).sqlExpression))
|
||||
}
|
||||
|
||||
/// Appends a raw `SQL` literal.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// "SELECT * FROM player WHERE \(literal: "name = \("O'Brien")")"
|
||||
public mutating func appendInterpolation(literal sqlLiteral: SQL) {
|
||||
elements.append(contentsOf: sqlLiteral.elements)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/// An SQL request that can decode database rows.
|
||||
///
|
||||
/// `SQLRequest` allows you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// extension Player: FetchableRecord {
|
||||
/// static func filter(name: String) -> SQLRequest<Player> {
|
||||
/// "SELECT * FROM player WHERE name = \(name)"
|
||||
/// }
|
||||
///
|
||||
/// static func maximumScore() -> SQLRequest<Int> {
|
||||
/// "SELECT MAX(score) FROM player"
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// try dbQueue.read { db in
|
||||
/// let players = try Player.filter(name: "O'Brien").fetchAll(db) // [Player]
|
||||
/// let maxScore = try Player.maximumScore().fetchOne(db) // Int?
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// An `SQLRequest` can be created from a string literal or interpolation, as in
|
||||
/// the above examples, and from the initializers documented below.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating an SQL Request from a Literal Value
|
||||
///
|
||||
/// - ``init(stringLiteral:)``
|
||||
/// - ``init(unicodeScalarLiteral:)-84mq8``
|
||||
/// - ``init(extendedGraphemeClusterLiteral:)-1sf75``
|
||||
///
|
||||
/// ### Creating an SQL Request from an Interpolation
|
||||
///
|
||||
/// - ``init(stringInterpolation:)``
|
||||
///
|
||||
/// ### Creating an SQL Request from an SQL Literal
|
||||
///
|
||||
/// - ``init(literal:adapter:cached:)-4vuxn``
|
||||
/// - ``init(literal:adapter:cached:)-82f97``
|
||||
///
|
||||
/// ### Creating an SQL Request from an SQL String
|
||||
///
|
||||
/// - ``init(sql:arguments:adapter:cached:)-3qq8t``
|
||||
/// - ``init(sql:arguments:adapter:cached:)-5ecx2``
|
||||
public struct SQLRequest<RowDecoder> {
|
||||
/// There are two statement caches: one "public" for statements generated by
|
||||
/// the user, and one "internal" for the statements generated by GRDB. Those
|
||||
/// are separated so that GRDB has no opportunity to inadvertently modify
|
||||
/// the arguments of user's cached statements.
|
||||
enum Cache {
|
||||
/// The public cache, for library user
|
||||
case `public`
|
||||
|
||||
/// The internal cache, for GRDB
|
||||
case `internal`
|
||||
}
|
||||
|
||||
/// The row adapter.
|
||||
public var adapter: (any RowAdapter)?
|
||||
|
||||
private(set) var sqlLiteral: SQL
|
||||
let cache: Cache?
|
||||
|
||||
private init(
|
||||
literal sqlLiteral: SQL,
|
||||
adapter: (any RowAdapter)?,
|
||||
fromCache cache: Cache?,
|
||||
type: RowDecoder.Type)
|
||||
{
|
||||
self.sqlLiteral = sqlLiteral
|
||||
self.adapter = adapter
|
||||
self.cache = cache
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLRequest {
|
||||
/// Creates a request from an SQL string.
|
||||
///
|
||||
/// ```swift
|
||||
/// let request = SQLRequest<String>(sql: """
|
||||
/// SELECT name FROM player
|
||||
/// """)
|
||||
/// let request = SQLRequest<Player>(sql: """
|
||||
/// SELECT * FROM player WHERE name = ?
|
||||
/// """, arguments: ["O'Brien"])
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - adapter: Optional RowAdapter.
|
||||
/// - cached: Defaults to false. If true, the request reuses a cached
|
||||
/// prepared statement.
|
||||
public init(
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments(),
|
||||
adapter: (any RowAdapter)? = nil,
|
||||
cached: Bool = false)
|
||||
{
|
||||
self.init(
|
||||
literal: SQL(sql: sql, arguments: arguments),
|
||||
adapter: adapter,
|
||||
fromCache: cached ? .public : nil,
|
||||
type: RowDecoder.self)
|
||||
}
|
||||
|
||||
/// Creates a request from an ``SQL`` literal.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// let name = "O'Brien"
|
||||
/// let request = SQLRequest<Player>(literal: """
|
||||
/// SELECT * FROM player WHERE name = \(name)
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqlLiteral: An `SQL` literal.
|
||||
/// - adapter: Optional RowAdapter.
|
||||
/// - cached: Defaults to false. If true, the request reuses a cached
|
||||
/// prepared statement.
|
||||
public init(literal sqlLiteral: SQL, adapter: (any RowAdapter)? = nil, cached: Bool = false) {
|
||||
self.init(
|
||||
literal: sqlLiteral,
|
||||
adapter: adapter,
|
||||
fromCache: cached ? .public : nil,
|
||||
type: RowDecoder.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLRequest<Row> {
|
||||
/// Creates a request of database rows, from an SQL string.
|
||||
///
|
||||
/// ```swift
|
||||
/// let request = SQLRequest(sql: """
|
||||
/// SELECT * FROM player WHERE name = ?
|
||||
/// """, arguments: ["O'Brien"])
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - adapter: Optional RowAdapter.
|
||||
/// - cached: Defaults to false. If true, the request reuses a cached
|
||||
/// prepared statement.
|
||||
public init(
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments(),
|
||||
adapter: (any RowAdapter)? = nil,
|
||||
cached: Bool = false)
|
||||
{
|
||||
self.init(
|
||||
literal: SQL(sql: sql, arguments: arguments),
|
||||
adapter: adapter,
|
||||
fromCache: cached ? .public : nil,
|
||||
type: Row.self)
|
||||
}
|
||||
|
||||
/// Creates a request of database rows, from an ``SQL`` literal.
|
||||
///
|
||||
/// ``SQL`` literals allow you to safely embed raw values in your SQL,
|
||||
/// without any risk of syntax errors or SQL injection:
|
||||
///
|
||||
/// ```swift
|
||||
/// let name = "O'Brien"
|
||||
/// let request = SQLRequest(literal: """
|
||||
/// SELECT * FROM player WHERE name = \(name)
|
||||
/// """)
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqlLiteral: An `SQL` literal.
|
||||
/// - adapter: Optional RowAdapter.
|
||||
/// - cached: Defaults to false. If true, the request reuses a cached
|
||||
/// prepared statement.
|
||||
public init(literal sqlLiteral: SQL, adapter: (any RowAdapter)? = nil, cached: Bool = false) {
|
||||
self.init(
|
||||
literal: sqlLiteral,
|
||||
adapter: adapter,
|
||||
fromCache: cached ? .public : nil,
|
||||
type: Row.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLRequest: FetchRequest {
|
||||
public var sqlSubquery: SQLSubquery {
|
||||
.literal(sqlLiteral)
|
||||
}
|
||||
|
||||
public func fetchCount(_ db: Database) throws -> Int {
|
||||
try SQLRequest<Int>("SELECT COUNT(*) FROM (\(self))").fetchOne(db)!
|
||||
}
|
||||
|
||||
public func makePreparedRequest(
|
||||
_ db: Database,
|
||||
forSingleResult singleResult: Bool = false)
|
||||
throws -> PreparedRequest
|
||||
{
|
||||
let context = SQLGenerationContext(db)
|
||||
let sql = try sqlLiteral.sql(context)
|
||||
let statement: Statement
|
||||
switch cache {
|
||||
case .none:
|
||||
statement = try db.makeStatement(sql: sql)
|
||||
case .public:
|
||||
statement = try db.cachedStatement(sql: sql)
|
||||
case .internal:
|
||||
statement = try db.internalCachedStatement(sql: sql)
|
||||
}
|
||||
try statement.setArguments(context.arguments)
|
||||
return PreparedRequest(statement: statement, adapter: adapter)
|
||||
}
|
||||
}
|
||||
|
||||
extension SQLRequest: ExpressibleByStringInterpolation {
|
||||
public init(unicodeScalarLiteral: String) {
|
||||
self.init(sql: unicodeScalarLiteral)
|
||||
}
|
||||
|
||||
public init(extendedGraphemeClusterLiteral: String) {
|
||||
self.init(sql: extendedGraphemeClusterLiteral)
|
||||
}
|
||||
|
||||
/// Creates an `SQLRequest` from the given literal SQL string.
|
||||
public init(stringLiteral: String) {
|
||||
self.init(sql: stringLiteral)
|
||||
}
|
||||
|
||||
public init(stringInterpolation sqlInterpolation: SQLInterpolation) {
|
||||
self.init(literal: SQL(stringInterpolation: sqlInterpolation))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import Dispatch
|
||||
|
||||
/// SchedulingWatchdog makes sure that databases connections are used on correct
|
||||
/// dispatch queues, and warns the user with a fatal error whenever she misuses
|
||||
/// a database connection.
|
||||
///
|
||||
/// Generally speaking, each connection has its own dispatch queue. But it's not
|
||||
/// enough: users need to use two database connections at the same time:
|
||||
/// <https://github.com/groue/GRDB.swift/issues/55>. To support this use case, a
|
||||
/// single dispatch queue can be temporarily shared by two or more connections.
|
||||
///
|
||||
/// - SchedulingWatchdog.makeSerializedQueue(allowingDatabase:) creates a
|
||||
/// dispatch queue that allows one database.
|
||||
///
|
||||
/// It does so by registering one instance of SchedulingWatchdog as a specific
|
||||
/// of the dispatch queue, a SchedulingWatchdog that allows that database only.
|
||||
///
|
||||
/// Later on, the queue can be shared by several databases with the method
|
||||
/// inheritingAllowedDatabases(from:execute:). See SerializedDatabase.sync()
|
||||
/// for an example.
|
||||
///
|
||||
/// - preconditionValidQueue() crashes whenever a database is used in an invalid
|
||||
/// dispatch queue.
|
||||
final class SchedulingWatchdog {
|
||||
private static let watchDogKey = DispatchSpecificKey<SchedulingWatchdog>()
|
||||
private(set) var allowedDatabases: [Database]
|
||||
var databaseObservationBroker: DatabaseObservationBroker?
|
||||
|
||||
private init(allowedDatabase database: Database) {
|
||||
allowedDatabases = [database]
|
||||
}
|
||||
|
||||
static func allowDatabase(_ database: Database, onQueue queue: DispatchQueue) {
|
||||
precondition(queue.getSpecific(key: watchDogKey) == nil)
|
||||
let watchdog = SchedulingWatchdog(allowedDatabase: database)
|
||||
queue.setSpecific(key: watchDogKey, value: watchdog)
|
||||
}
|
||||
|
||||
func inheritingAllowedDatabases<T>(from other: SchedulingWatchdog, execute body: () throws -> T) rethrows -> T {
|
||||
let backup = allowedDatabases
|
||||
allowedDatabases.append(contentsOf: other.allowedDatabases)
|
||||
defer { allowedDatabases = backup }
|
||||
return try body()
|
||||
}
|
||||
|
||||
static func preconditionValidQueue(
|
||||
_ db: Database,
|
||||
_ message: @autoclosure() -> String = "Database was not used on the correct thread.",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
{
|
||||
GRDBPrecondition(allows(db), message(), file: file, line: line)
|
||||
}
|
||||
|
||||
/// Returns whether the database argument can be used in the current
|
||||
/// dispatch queue.
|
||||
static func allows(_ db: Database) -> Bool {
|
||||
current?.allows(db) ?? false
|
||||
}
|
||||
|
||||
static var current: SchedulingWatchdog? {
|
||||
DispatchQueue.getSpecific(key: watchDogKey)
|
||||
}
|
||||
|
||||
func allows(_ db: Database) -> Bool {
|
||||
allowedDatabases.contains { $0 === db }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import Foundation
|
||||
|
||||
/// A class that serializes accesses to an SQLite connection.
|
||||
final class SerializedDatabase {
|
||||
/// The database connection
|
||||
private let db: Database
|
||||
|
||||
/// The database configuration
|
||||
var configuration: Configuration { db.configuration }
|
||||
|
||||
/// The path to the database file
|
||||
let path: String
|
||||
|
||||
/// The dispatch queue
|
||||
private let queue: DispatchQueue
|
||||
|
||||
/// If true, overrides `configuration.allowsUnsafeTransactions`.
|
||||
private var allowsUnsafeTransactions = false
|
||||
|
||||
init(
|
||||
path: String,
|
||||
configuration: Configuration = Configuration(),
|
||||
defaultLabel: String,
|
||||
purpose: String? = nil)
|
||||
throws
|
||||
{
|
||||
// According to https://www.sqlite.org/threadsafe.html
|
||||
//
|
||||
// > SQLite support three different threading modes:
|
||||
// >
|
||||
// > 1. Multi-thread. In this mode, SQLite can be safely used by
|
||||
// > multiple threads provided that no single database connection is
|
||||
// > used simultaneously in two or more threads.
|
||||
// >
|
||||
// > 2. Serialized. In serialized mode, SQLite can be safely used by
|
||||
// > multiple threads with no restriction.
|
||||
// >
|
||||
// > [...]
|
||||
// >
|
||||
// > The default mode is serialized.
|
||||
//
|
||||
// Since our database connection is only used via our serial dispatch
|
||||
// queue, there is no purpose using the default serialized mode.
|
||||
var config = configuration
|
||||
config.threadingMode = .multiThread
|
||||
|
||||
self.path = path
|
||||
let identifier = configuration.identifier(defaultLabel: defaultLabel, purpose: purpose)
|
||||
self.db = try Database(
|
||||
path: path,
|
||||
description: identifier,
|
||||
configuration: config)
|
||||
if config.readonly {
|
||||
self.queue = configuration.makeReaderDispatchQueue(label: identifier)
|
||||
} else {
|
||||
self.queue = configuration.makeWriterDispatchQueue(label: identifier)
|
||||
}
|
||||
SchedulingWatchdog.allowDatabase(db, onQueue: queue)
|
||||
try queue.sync {
|
||||
do {
|
||||
try db.setUp()
|
||||
} catch {
|
||||
// Recent versions of the Swift compiler will call the
|
||||
// deinitializer. Older ones won't.
|
||||
// See https://bugs.swift.org/browse/SR-13746 for details.
|
||||
//
|
||||
// So let's close the database now. The deinitializer
|
||||
// will only close the database if needed.
|
||||
db.close_v2()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Database may be deallocated in its own queue: allow reentrancy
|
||||
reentrantSync { db in
|
||||
db.close_v2()
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes database operations, returns their result after they have
|
||||
/// finished executing, and allows or forbids long-lived transactions.
|
||||
///
|
||||
/// This method is not reentrant.
|
||||
///
|
||||
/// - parameter allowingLongLivedTransaction: When true, the
|
||||
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
|
||||
/// ignored until this method is called again with false.
|
||||
func sync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
|
||||
try sync { db in
|
||||
self.allowsUnsafeTransactions = allowingLongLivedTransaction
|
||||
return try body(db)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes database operations, and returns their result after they
|
||||
/// have finished executing.
|
||||
///
|
||||
/// This method is not reentrant.
|
||||
func sync<T>(_ block: (Database) throws -> T) rethrows -> T {
|
||||
// Three different cases:
|
||||
//
|
||||
// 1. A database is invoked from some queue like the main queue:
|
||||
//
|
||||
// serializedDatabase.sync { db in // <-- we're here
|
||||
// }
|
||||
//
|
||||
// 2. A database is invoked in a reentrant way:
|
||||
//
|
||||
// serializedDatabase.sync { db in
|
||||
// serializedDatabase.sync { db in // <-- we're here
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 3. A database in invoked from another database:
|
||||
//
|
||||
// serializedDatabase1.sync { db1 in
|
||||
// serializedDatabase2.sync { db2 in // <-- we're here
|
||||
// }
|
||||
// }
|
||||
|
||||
guard let watchdog = SchedulingWatchdog.current else {
|
||||
// Case 1
|
||||
return try queue.sync {
|
||||
defer { preconditionNoUnsafeTransactionLeft(db) }
|
||||
return try block(db)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2 is forbidden.
|
||||
GRDBPrecondition(!watchdog.allows(db), "Database methods are not reentrant.")
|
||||
|
||||
// Case 3
|
||||
return try queue.sync {
|
||||
try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) {
|
||||
defer { preconditionNoUnsafeTransactionLeft(db) }
|
||||
return try block(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes database operations, returns their result after they have
|
||||
/// finished executing, and allows or forbids long-lived transactions.
|
||||
///
|
||||
/// This method is reentrant.
|
||||
///
|
||||
/// - parameter allowingLongLivedTransaction: When true, the
|
||||
/// ``Configuration/allowsUnsafeTransactions`` configuration flag is
|
||||
/// ignored until this method is called again with false.
|
||||
func reentrantSync<T>(allowingLongLivedTransaction: Bool, _ body: (Database) throws -> T) rethrows -> T {
|
||||
try reentrantSync { db in
|
||||
self.allowsUnsafeTransactions = allowingLongLivedTransaction
|
||||
return try body(db)
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes database operations, and returns their result after they
|
||||
/// have finished executing.
|
||||
///
|
||||
/// This method is reentrant.
|
||||
func reentrantSync<T>(_ block: (Database) throws -> T) rethrows -> T {
|
||||
// Three different cases:
|
||||
//
|
||||
// 1. A database is invoked from some queue like the main queue:
|
||||
//
|
||||
// serializedDatabase.reentrantSync { db in // <-- we're here
|
||||
// }
|
||||
//
|
||||
// 2. A database is invoked in a reentrant way:
|
||||
//
|
||||
// serializedDatabase.reentrantSync { db in
|
||||
// serializedDatabase.reentrantSync { db in // <-- we're here
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 3. A database in invoked from another database:
|
||||
//
|
||||
// serializedDatabase1.reentrantSync { db1 in
|
||||
// serializedDatabase2.reentrantSync { db2 in // <-- we're here
|
||||
// }
|
||||
// }
|
||||
|
||||
guard let watchdog = SchedulingWatchdog.current else {
|
||||
// Case 1
|
||||
return try queue.sync {
|
||||
// Since we are reentrant, a transaction may already be opened.
|
||||
// In this case, don't check for unsafe transaction at the end.
|
||||
if db.isInsideTransaction {
|
||||
return try block(db)
|
||||
} else {
|
||||
defer { preconditionNoUnsafeTransactionLeft(db) }
|
||||
return try block(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2
|
||||
if watchdog.allows(db) {
|
||||
// Since we are reentrant, a transaction may already be opened.
|
||||
// In this case, don't check for unsafe transaction at the end.
|
||||
if db.isInsideTransaction {
|
||||
return try block(db)
|
||||
} else {
|
||||
defer { preconditionNoUnsafeTransactionLeft(db) }
|
||||
return try block(db)
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3
|
||||
return try queue.sync {
|
||||
try SchedulingWatchdog.current!.inheritingAllowedDatabases(from: watchdog) {
|
||||
// Since we are reentrant, a transaction may already be opened.
|
||||
// In this case, don't check for unsafe transaction at the end.
|
||||
if db.isInsideTransaction {
|
||||
return try block(db)
|
||||
} else {
|
||||
defer { preconditionNoUnsafeTransactionLeft(db) }
|
||||
return try block(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules database operations for execution, and returns immediately.
|
||||
func async(_ block: @escaping (Database) -> Void) {
|
||||
queue.async {
|
||||
block(self.db)
|
||||
self.preconditionNoUnsafeTransactionLeft(self.db)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if any only if the current dispatch queue is valid.
|
||||
var onValidQueue: Bool {
|
||||
SchedulingWatchdog.current?.allows(db) ?? false
|
||||
}
|
||||
|
||||
/// Executes the block in the current queue.
|
||||
///
|
||||
/// - precondition: the current dispatch queue is valid.
|
||||
func execute<T>(_ block: (Database) throws -> T) rethrows -> T {
|
||||
preconditionValidQueue()
|
||||
return try block(db)
|
||||
}
|
||||
|
||||
func interrupt() {
|
||||
// Intentionally not scheduled in our serial queue
|
||||
db.interrupt()
|
||||
}
|
||||
|
||||
func suspend() {
|
||||
// Intentionally not scheduled in our serial queue
|
||||
db.suspend()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
// Intentionally not scheduled in our serial queue
|
||||
db.resume()
|
||||
}
|
||||
|
||||
/// Fatal error if current dispatch queue is not valid.
|
||||
func preconditionValidQueue(
|
||||
_ message: @autoclosure() -> String = "Database was not used on the correct thread.",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
{
|
||||
SchedulingWatchdog.preconditionValidQueue(db, message(), file: file, line: line)
|
||||
}
|
||||
|
||||
/// Fatal error if a transaction has been left opened.
|
||||
private func preconditionNoUnsafeTransactionLeft(
|
||||
_ db: Database,
|
||||
_ message: @autoclosure() -> String = "A transaction has been left opened at the end of a database access",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line)
|
||||
{
|
||||
GRDBPrecondition(
|
||||
allowsUnsafeTransactions || configuration.allowsUnsafeTransactions || !db.isInsideTransaction,
|
||||
message(),
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
// @unchecked because the wrapped `Database` itself is not Sendable.
|
||||
// It happens the job of SerializedDatabase is precisely to provide thread-safe
|
||||
// access to `Database`.
|
||||
extension SerializedDatabase: @unchecked Sendable { }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,244 @@
|
||||
#if canImport(string_h)
|
||||
import string_h
|
||||
#elseif os(Linux)
|
||||
import Glibc
|
||||
#elseif os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS)
|
||||
import Darwin
|
||||
#endif
|
||||
|
||||
/// `StatementAuthorizer` provides information about compiled database
|
||||
/// statements, and prevents the truncate optimization when row deletions are
|
||||
/// observed by transaction observers.
|
||||
///
|
||||
/// <https://www.sqlite.org/c3ref/set_authorizer.html>
|
||||
/// <https://www.sqlite.org/lang_delete.html#the_truncate_optimization>
|
||||
final class StatementAuthorizer {
|
||||
private unowned var database: Database
|
||||
|
||||
/// What a statement reads.
|
||||
var selectedRegion = DatabaseRegion()
|
||||
|
||||
/// What a statement writes.
|
||||
var databaseEventKinds: [DatabaseEventKind] = []
|
||||
|
||||
/// True if a statement alters the schema in a way that requires
|
||||
/// invalidation of the schema cache. For example, adding a column to a
|
||||
/// table invalidates the schema cache.
|
||||
var invalidatesDatabaseSchemaCache = false
|
||||
|
||||
/// Not nil if a statement is a BEGIN/COMMIT/ROLLBACK/RELEASE transaction or
|
||||
/// savepoint statement.
|
||||
var transactionEffect: Statement.TransactionEffect?
|
||||
|
||||
private var isDropStatement = false
|
||||
|
||||
init(_ database: Database) {
|
||||
self.database = database
|
||||
}
|
||||
|
||||
/// Registers the authorizer with `sqlite3_set_authorizer`.
|
||||
func register() {
|
||||
let authorizerP = Unmanaged.passUnretained(self).toOpaque()
|
||||
sqlite3_set_authorizer(
|
||||
database.sqliteConnection,
|
||||
{ (authorizerP, actionCode, cString1, cString2, cString3, cString4) in
|
||||
Unmanaged<StatementAuthorizer>
|
||||
.fromOpaque(authorizerP.unsafelyUnwrapped)
|
||||
.takeUnretainedValue()
|
||||
.authorize(actionCode, cString1, cString2, cString3, cString4)
|
||||
},
|
||||
authorizerP)
|
||||
}
|
||||
|
||||
/// Reset before compiling a new statement
|
||||
func reset() {
|
||||
selectedRegion = DatabaseRegion()
|
||||
databaseEventKinds = []
|
||||
invalidatesDatabaseSchemaCache = false
|
||||
transactionEffect = nil
|
||||
isDropStatement = false
|
||||
}
|
||||
|
||||
private func authorize(
|
||||
_ actionCode: CInt,
|
||||
_ cString1: UnsafePointer<CChar>?,
|
||||
_ cString2: UnsafePointer<CChar>?,
|
||||
_ cString3: UnsafePointer<CChar>?,
|
||||
_ cString4: UnsafePointer<CChar>?)
|
||||
-> CInt
|
||||
{
|
||||
// Uncomment when debugging
|
||||
// print("""
|
||||
// StatementAuthorizer: \
|
||||
// \(AuthorizerActionCode(rawValue: actionCode)) \
|
||||
// \([cString1, cString2, cString3, cString4].compactMap { $0.map(String.init) }.joined(separator: ", "))
|
||||
// """)
|
||||
|
||||
switch actionCode {
|
||||
case SQLITE_DROP_TABLE, SQLITE_DROP_VTABLE, SQLITE_DROP_TEMP_TABLE,
|
||||
SQLITE_DROP_INDEX, SQLITE_DROP_TEMP_INDEX,
|
||||
SQLITE_DROP_VIEW, SQLITE_DROP_TEMP_VIEW,
|
||||
SQLITE_DROP_TRIGGER, SQLITE_DROP_TEMP_TRIGGER:
|
||||
isDropStatement = true
|
||||
invalidatesDatabaseSchemaCache = true
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_ATTACH, SQLITE_DETACH, SQLITE_ALTER_TABLE,
|
||||
SQLITE_CREATE_INDEX, SQLITE_CREATE_TABLE,
|
||||
SQLITE_CREATE_TEMP_INDEX, SQLITE_CREATE_TEMP_TABLE,
|
||||
SQLITE_CREATE_TEMP_TRIGGER, SQLITE_CREATE_TEMP_VIEW,
|
||||
SQLITE_CREATE_TRIGGER, SQLITE_CREATE_VIEW,
|
||||
SQLITE_CREATE_VTABLE:
|
||||
invalidatesDatabaseSchemaCache = true
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_READ:
|
||||
guard let tableName = cString1.map(String.init) else { return SQLITE_OK }
|
||||
guard let columnName = cString2.map(String.init) else { return SQLITE_OK }
|
||||
if columnName.isEmpty {
|
||||
// SELECT COUNT(*) FROM table
|
||||
selectedRegion.formUnion(DatabaseRegion(table: tableName))
|
||||
} else {
|
||||
// SELECT column FROM table
|
||||
selectedRegion.formUnion(DatabaseRegion(table: tableName, columns: [columnName]))
|
||||
}
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_INSERT:
|
||||
guard let tableName = cString1.map(String.init) else { return SQLITE_OK }
|
||||
databaseEventKinds.append(.insert(tableName: tableName))
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_DELETE:
|
||||
if isDropStatement { return SQLITE_OK }
|
||||
guard let cString1 else { return SQLITE_OK }
|
||||
|
||||
// Deletions from sqlite_master and sqlite_temp_master are not like
|
||||
// other deletions: `sqlite3_update_hook` does not notify them, and
|
||||
// they are prevented when the truncate optimization is disabled.
|
||||
// Let's always authorize such deletions by returning SQLITE_OK:
|
||||
guard strcmp(cString1, "sqlite_master") != 0 else { return SQLITE_OK }
|
||||
guard strcmp(cString1, "sqlite_temp_master") != 0 else { return SQLITE_OK }
|
||||
|
||||
let tableName = String(cString: cString1)
|
||||
databaseEventKinds.append(.delete(tableName: tableName))
|
||||
|
||||
if let observationBroker = database.observationBroker,
|
||||
observationBroker.observesDeletions(on: tableName)
|
||||
{
|
||||
// Prevent the truncate optimization so that
|
||||
// `sqlite3_update_hook` notifies individual row deletions to
|
||||
// transaction observers.
|
||||
return SQLITE_IGNORE
|
||||
} else {
|
||||
return SQLITE_OK
|
||||
}
|
||||
|
||||
case SQLITE_UPDATE:
|
||||
guard let tableName = cString1.map(String.init) else { return SQLITE_OK }
|
||||
guard let columnName = cString2.map(String.init) else { return SQLITE_OK }
|
||||
insertUpdateEventKind(tableName: tableName, columnName: columnName)
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_TRANSACTION:
|
||||
guard let cString1 else { return SQLITE_OK }
|
||||
if strcmp(cString1, "BEGIN") == 0 {
|
||||
transactionEffect = .beginTransaction
|
||||
} else if strcmp(cString1, "COMMIT") == 0 {
|
||||
transactionEffect = .commitTransaction
|
||||
} else if strcmp(cString1, "ROLLBACK") == 0 {
|
||||
transactionEffect = .rollbackTransaction
|
||||
}
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_SAVEPOINT:
|
||||
guard let cString1 else { return SQLITE_OK }
|
||||
guard let name = cString2.map(String.init) else { return SQLITE_OK }
|
||||
if strcmp(cString1, "BEGIN") == 0 {
|
||||
transactionEffect = .beginSavepoint(name)
|
||||
} else if strcmp(cString1, "RELEASE") == 0 {
|
||||
transactionEffect = .releaseSavepoint(name)
|
||||
} else if strcmp(cString1, "ROLLBACK") == 0 {
|
||||
transactionEffect = .rollbackSavepoint(name)
|
||||
}
|
||||
return SQLITE_OK
|
||||
|
||||
case SQLITE_FUNCTION:
|
||||
// SQLite 3.37.2 does not report ALTER TABLE DROP COLUMN with the
|
||||
// SQLITE_ALTER_TABLE action code. SQLite 3.38 does.
|
||||
//
|
||||
// Until SQLite 3.38, we need to find another way to set the
|
||||
// `invalidatesDatabaseSchemaCache` flag for such
|
||||
// statement, and it is SQLITE_FUNCTION sqlite_drop_column.
|
||||
//
|
||||
// See <https://github.com/groue/GRDB.swift/pull/1144#issuecomment-1015155717>
|
||||
// See <https://sqlite.org/forum/forumpost/bd47580ec2>
|
||||
if sqlite3_libversion_number() < 3038000,
|
||||
let cString2,
|
||||
strcmp(cString2, "sqlite_drop_column") == 0
|
||||
{
|
||||
invalidatesDatabaseSchemaCache = true
|
||||
}
|
||||
return SQLITE_OK
|
||||
|
||||
default:
|
||||
return SQLITE_OK
|
||||
}
|
||||
}
|
||||
|
||||
private func insertUpdateEventKind(tableName: String, columnName: String) {
|
||||
for (index, eventKind) in databaseEventKinds.enumerated() {
|
||||
if case .update(let t, let columnNames) = eventKind, t == tableName {
|
||||
var columnNames = columnNames
|
||||
columnNames.insert(columnName)
|
||||
databaseEventKinds[index] = .update(tableName: tableName, columnNames: columnNames)
|
||||
return
|
||||
}
|
||||
}
|
||||
databaseEventKinds.append(.update(tableName: tableName, columnNames: [columnName]))
|
||||
}
|
||||
}
|
||||
|
||||
private struct AuthorizerActionCode: RawRepresentable, CustomStringConvertible {
|
||||
let rawValue: CInt
|
||||
|
||||
var description: String {
|
||||
switch rawValue {
|
||||
case 1: return "SQLITE_CREATE_INDEX"
|
||||
case 2: return "SQLITE_CREATE_TABLE"
|
||||
case 3: return "SQLITE_CREATE_TEMP_INDEX"
|
||||
case 4: return "SQLITE_CREATE_TEMP_TABLE"
|
||||
case 5: return "SQLITE_CREATE_TEMP_TRIGGER"
|
||||
case 6: return "SQLITE_CREATE_TEMP_VIEW"
|
||||
case 7: return "SQLITE_CREATE_TRIGGER"
|
||||
case 8: return "SQLITE_CREATE_VIEW"
|
||||
case 9: return "SQLITE_DELETE"
|
||||
case 10: return "SQLITE_DROP_INDEX"
|
||||
case 11: return "SQLITE_DROP_TABLE"
|
||||
case 12: return "SQLITE_DROP_TEMP_INDEX"
|
||||
case 13: return "SQLITE_DROP_TEMP_TABLE"
|
||||
case 14: return "SQLITE_DROP_TEMP_TRIGGER"
|
||||
case 15: return "SQLITE_DROP_TEMP_VIEW"
|
||||
case 16: return "SQLITE_DROP_TRIGGER"
|
||||
case 17: return "SQLITE_DROP_VIEW"
|
||||
case 18: return "SQLITE_INSERT"
|
||||
case 19: return "SQLITE_PRAGMA"
|
||||
case 20: return "SQLITE_READ"
|
||||
case 21: return "SQLITE_SELECT"
|
||||
case 22: return "SQLITE_TRANSACTION"
|
||||
case 23: return "SQLITE_UPDATE"
|
||||
case 24: return "SQLITE_ATTACH"
|
||||
case 25: return "SQLITE_DETACH"
|
||||
case 26: return "SQLITE_ALTER_TABLE"
|
||||
case 27: return "SQLITE_REINDEX"
|
||||
case 28: return "SQLITE_ANALYZE"
|
||||
case 29: return "SQLITE_CREATE_VTABLE"
|
||||
case 30: return "SQLITE_DROP_VTABLE"
|
||||
case 31: return "SQLITE_FUNCTION"
|
||||
case 32: return "SQLITE_SAVEPOINT"
|
||||
case 0: return "SQLITE_COPY"
|
||||
case 33: return "SQLITE_RECURSIVE"
|
||||
default: return "\(rawValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
/// A type that can decode itself from the low-level C interface to
|
||||
/// SQLite results.
|
||||
///
|
||||
/// `StatementColumnConvertible` is adopted by `Bool`, `Int`, `String`,
|
||||
/// `Date`, and most common values.
|
||||
///
|
||||
/// When a type conforms to both ``DatabaseValueConvertible`` and
|
||||
/// `StatementColumnConvertible`, GRDB can apply some optimization whenever
|
||||
/// direct access to SQLite is possible. For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // Optimized
|
||||
/// let scores = Int.fetchAll(db, sql: "SELECT score FROM player")
|
||||
///
|
||||
/// let rows = try Row.fetchCursor(db, sql: "SELECT * FROM player")
|
||||
/// while let row = try rows.next() {
|
||||
/// // Optimized
|
||||
/// let int: Int = row[0]
|
||||
/// let name: String = row[1]
|
||||
/// }
|
||||
///
|
||||
/// struct Player: FetchableRecord {
|
||||
/// var name: String
|
||||
/// var score: Int
|
||||
///
|
||||
/// init(row: Row) {
|
||||
/// // Optimized
|
||||
/// name = row["name"]
|
||||
/// score = row["score"]
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// To conform to `StatementColumnConvertible`, provide a custom implementation
|
||||
/// of ``init(sqliteStatement:index:)-354je``. This implementation is ready-made
|
||||
/// for `RawRepresentable` types whose `RawValue`
|
||||
/// is `StatementColumnConvertible`.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/column_blob.html>
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating a Value
|
||||
///
|
||||
/// - ``init(sqliteStatement:index:)-354je``
|
||||
/// - ``fromStatement(_:atUncheckedIndex:)-2i8y6``
|
||||
///
|
||||
/// ### Fetching Values from Raw SQL
|
||||
///
|
||||
/// - ``DatabaseValueConvertible/fetchCursor(_:sql:arguments:adapter:)-4xfxh``
|
||||
/// - ``DatabaseValueConvertible/fetchAll(_:sql:arguments:adapter:)-7bn2i``
|
||||
/// - ``DatabaseValueConvertible/fetchSet(_:sql:arguments:adapter:)-1ythd``
|
||||
/// - ``DatabaseValueConvertible/fetchOne(_:sql:arguments:adapter:)-563lc``
|
||||
///
|
||||
/// ### Fetching Values from a Prepared Statement
|
||||
///
|
||||
/// - ``DatabaseValueConvertible/fetchCursor(_:arguments:adapter:)-81f9d``
|
||||
/// - ``DatabaseValueConvertible/fetchAll(_:arguments:adapter:)-64gua``
|
||||
/// - ``DatabaseValueConvertible/fetchSet(_:arguments:adapter:)-9fh2b``
|
||||
/// - ``DatabaseValueConvertible/fetchOne(_:arguments:adapter:)-8cbzp``
|
||||
///
|
||||
/// ### Fetching Values from a Request
|
||||
///
|
||||
/// - ``DatabaseValueConvertible/fetchCursor(_:_:)-77a34``
|
||||
/// - ``DatabaseValueConvertible/fetchAll(_:_:)-7tnun``
|
||||
/// - ``DatabaseValueConvertible/fetchSet(_:_:)-4bc1m``
|
||||
/// - ``DatabaseValueConvertible/fetchOne(_:_:)-94q4e``
|
||||
///
|
||||
/// ### Supporting Types
|
||||
///
|
||||
/// - ``FastDatabaseValueCursor``
|
||||
public protocol StatementColumnConvertible {
|
||||
/// Creates an instance from a raw SQLite statement pointer, if possible.
|
||||
///
|
||||
/// This method can be called with a NULL database value.
|
||||
///
|
||||
/// - warning: Do not customize the default implementation.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
/// - returns: A decoded value, or, if decoding is impossible, nil.
|
||||
static func fromStatement(
|
||||
_ sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt)
|
||||
-> Self?
|
||||
|
||||
/// Creates an instance from a raw SQLite statement pointer, if possible.
|
||||
///
|
||||
/// Do not check for `NULL` in your implementation of this method. Null
|
||||
/// database values are handled
|
||||
/// in ``StatementColumnConvertible/fromStatement(_:atUncheckedIndex:)-2i8y6``.
|
||||
///
|
||||
/// For example, here is the how Int64 adopts StatementColumnConvertible:
|
||||
///
|
||||
/// ```swift
|
||||
/// extension Int64: StatementColumnConvertible {
|
||||
/// public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
/// self = sqlite3_column_int64(sqliteStatement, index)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/column_blob.html>
|
||||
///
|
||||
/// - precondition: This initializer is not called with a NULL
|
||||
/// database value.
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
/// - returns: A decoded value, or, if decoding is impossible, nil.
|
||||
init?(sqliteStatement: SQLiteStatement, index: CInt)
|
||||
}
|
||||
|
||||
extension StatementColumnConvertible {
|
||||
// `Optional` overrides this default behavior.
|
||||
/// Default implementation fails on decoding NULL.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static func fromStatement(_ sqliteStatement: SQLiteStatement, atUncheckedIndex index: CInt) -> Self? {
|
||||
if sqlite3_column_type(sqliteStatement, index) == SQLITE_NULL {
|
||||
return nil
|
||||
}
|
||||
return self.init(sqliteStatement: sqliteStatement, index: index)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conversions
|
||||
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
|
||||
@usableFromInline
|
||||
/* private */ static func _valueMismatch(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Never
|
||||
{
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Self.self,
|
||||
sqliteStatement: sqliteStatement,
|
||||
index: index,
|
||||
context: context())
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
static func fastDecode(
|
||||
fromRow row: Row,
|
||||
atUncheckedIndex index: Int)
|
||||
throws -> Self
|
||||
{
|
||||
if let sqliteStatement = row.sqliteStatement {
|
||||
return try fastDecode(
|
||||
fromStatement: sqliteStatement,
|
||||
atUncheckedIndex: CInt(index),
|
||||
context: RowDecodingContext(row: row, key: .columnIndex(index)))
|
||||
}
|
||||
// Support for fast decoding from adapted rows
|
||||
return try row.fastDecode(Self.self, atUncheckedIndex: index)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
static func fastDecode(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Self
|
||||
{
|
||||
if let value = fromStatement(sqliteStatement, atUncheckedIndex: index) {
|
||||
return value
|
||||
} else {
|
||||
try _valueMismatch(fromStatement: sqliteStatement, atUncheckedIndex: index, context: context())
|
||||
}
|
||||
}
|
||||
|
||||
// Support for Decodable
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
static func fastDecodeIfPresent(
|
||||
fromRow row: Row,
|
||||
atUncheckedIndex index: Int)
|
||||
throws -> Self?
|
||||
{
|
||||
try Optional<Self>.fastDecode(fromRow: row, atUncheckedIndex: index)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cursors
|
||||
|
||||
/// A cursor of database values.
|
||||
///
|
||||
/// A `FastDatabaseValueCursor` iterates all rows from a database request. Its
|
||||
/// elements are the database values decoded from the leftmost column.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let names: FastDatabaseValueCursor<String> = try String.fetchCursor(db, sql: """
|
||||
/// SELECT name FROM player
|
||||
/// """)
|
||||
/// while let name = names.next() { // String
|
||||
/// print(name)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
public final class FastDatabaseValueCursor<Value>: DatabaseCursor
|
||||
where Value: DatabaseValueConvertible & StatementColumnConvertible
|
||||
{
|
||||
public typealias Element = Value
|
||||
public let _statement: Statement
|
||||
public var _isDone = false
|
||||
@usableFromInline let columnIndex: CInt
|
||||
|
||||
init(statement: Statement, arguments: StatementArguments? = nil, adapter: (any RowAdapter)? = nil) throws {
|
||||
self._statement = statement
|
||||
if let adapter {
|
||||
// adapter may redefine the index of the leftmost column
|
||||
columnIndex = try CInt(adapter.baseColumnIndex(atIndex: 0, layout: statement))
|
||||
} else {
|
||||
columnIndex = 0
|
||||
}
|
||||
|
||||
// Assume cursor is created for immediate iteration: reset and set arguments
|
||||
try statement.prepareExecution(withArguments: arguments)
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Statement reset fails when sqlite3_step has previously failed.
|
||||
// Just ignore reset error.
|
||||
try? _statement.reset()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func _element(sqliteStatement: SQLiteStatement) throws -> Value {
|
||||
try Value.fastDecode(
|
||||
fromStatement: sqliteStatement,
|
||||
atUncheckedIndex: columnIndex,
|
||||
context: RowDecodingContext(statement: _statement, index: Int(columnIndex)))
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit non-conformance to Sendable: database cursors must be used from
|
||||
// a serialized database access dispatch queue.
|
||||
@available(*, unavailable)
|
||||
extension FastDatabaseValueCursor: Sendable { }
|
||||
|
||||
/// Types that adopt both DatabaseValueConvertible and
|
||||
/// StatementColumnConvertible can be efficiently initialized from
|
||||
/// database values.
|
||||
///
|
||||
/// See DatabaseValueConvertible for more information.
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
|
||||
|
||||
// MARK: Fetching From Prepared Statement
|
||||
|
||||
/// Returns a cursor over values fetched from a prepared statement.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let statement = try db.makeStatement(sql: sql)
|
||||
/// let scores = try Int.fetchCursor(statement, arguments: [lastName])
|
||||
/// while let score = try scores.next() {
|
||||
/// print(score)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - statement: The statement to run.
|
||||
/// - arguments: Optional statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor(
|
||||
_ statement: Statement,
|
||||
arguments: StatementArguments? = nil,
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> FastDatabaseValueCursor<Self>
|
||||
{
|
||||
try FastDatabaseValueCursor(statement: statement, arguments: arguments, adapter: adapter)
|
||||
}
|
||||
|
||||
/// Returns an array of values fetched from a prepared statement.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let statement = try db.makeStatement(sql: sql)
|
||||
/// let scores = try Int.fetchAll(statement, arguments: [lastName])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - statement: The statement to run.
|
||||
/// - arguments: Optional statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: An array of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll(
|
||||
_ statement: Statement,
|
||||
arguments: StatementArguments? = nil,
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> [Self]
|
||||
{
|
||||
try Array(fetchCursor(statement, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
|
||||
/// Returns a single value fetched from a prepared statement.
|
||||
///
|
||||
/// The value is decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// The result is nil if the request returns no row, or one row with a
|
||||
/// `NULL` value.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ? LIMIT 1"
|
||||
/// let statement = try db.makeStatement(sql: sql)
|
||||
/// let score = try Int.fetchOne(statement, arguments: [lastName])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - statement: The statement to run.
|
||||
/// - arguments: Optional statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: An optional value.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(
|
||||
_ statement: Statement,
|
||||
arguments: StatementArguments? = nil,
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> Self?
|
||||
{
|
||||
// fetchOne handles both a missing row, and one row with a NULL value.
|
||||
let cursor = try FastDatabaseValueCursor<Self?>(
|
||||
statement: statement,
|
||||
arguments: arguments,
|
||||
adapter: adapter)
|
||||
return try cursor.next() ?? nil
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hashable {
|
||||
/// Returns a set of values fetched from a prepared statement.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let statement = try db.makeStatement(sql: sql)
|
||||
/// let scores = try Int.fetchSet(statement, arguments: [lastName])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - statement: The statement to run.
|
||||
/// - arguments: Optional statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: A set of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet(
|
||||
_ statement: Statement,
|
||||
arguments: StatementArguments? = nil,
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> Set<Self>
|
||||
{
|
||||
try Set(fetchCursor(statement, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
|
||||
|
||||
// MARK: Fetching From SQL
|
||||
|
||||
/// Returns a cursor over values fetched from an SQL query.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let scores = try Int.fetchCursor(db, sql: sql, arguments: [lastName])
|
||||
/// while let score = try scores.next() {
|
||||
/// print(score)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor(
|
||||
_ db: Database,
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments(),
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> FastDatabaseValueCursor<Self>
|
||||
{
|
||||
try fetchCursor(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
|
||||
/// Returns an array of values fetched from an SQL query.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let scores = try Int.fetchAll(db, sql: sql, arguments: [lastName])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: An array of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll(
|
||||
_ db: Database,
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments(),
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> [Self]
|
||||
{
|
||||
try fetchAll(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
|
||||
/// Returns a single value fetched from an SQL query.
|
||||
///
|
||||
/// The value is decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// The result is nil if the request returns no row, or one row with a
|
||||
/// `NULL` value.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let score = try Int.fetchOne(db, sql: sql, arguments: [lastName])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: An optional value.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(
|
||||
_ db: Database,
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments(),
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> Self?
|
||||
{
|
||||
try fetchOne(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hashable {
|
||||
/// Returns a set of values fetched from an SQL query.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
/// let sql = "SELECT score FROM player WHERE lastName = ?"
|
||||
/// let scores = try Int.fetchSet(db, sql: sql, arguments: [lastName])
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column if the `adapter` argument
|
||||
/// is nil.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - sql: An SQL string.
|
||||
/// - arguments: Statement arguments.
|
||||
/// - adapter: Optional RowAdapter
|
||||
/// - returns: A set of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet(
|
||||
_ db: Database,
|
||||
sql: String,
|
||||
arguments: StatementArguments = StatementArguments(),
|
||||
adapter: (any RowAdapter)? = nil)
|
||||
throws -> Set<Self>
|
||||
{
|
||||
try fetchSet(db, SQLRequest(sql: sql, arguments: arguments, adapter: adapter))
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible {
|
||||
|
||||
// MARK: Fetching From FetchRequest
|
||||
|
||||
/// Returns a cursor over values fetched from a fetch request.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .select(Column("score"))
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// let scores = try Int.fetchCursor(db, request)
|
||||
/// while let score = try scores.next() {
|
||||
/// print(score)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column.
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - request: A FetchRequest.
|
||||
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchCursor(_ db: Database, _ request: some FetchRequest)
|
||||
throws -> FastDatabaseValueCursor<Self>
|
||||
{
|
||||
let request = try request.makePreparedRequest(db, forSingleResult: false)
|
||||
return try fetchCursor(request.statement, adapter: request.adapter)
|
||||
}
|
||||
|
||||
/// Returns an array of values fetched from a fetch request.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .select(Column("score"))
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// let scores = try Int.fetchAll(db, request)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - request: A FetchRequest.
|
||||
/// - returns: An array of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchAll(_ db: Database, _ request: some FetchRequest) throws -> [Self] {
|
||||
let request = try request.makePreparedRequest(db, forSingleResult: false)
|
||||
return try fetchAll(request.statement, adapter: request.adapter)
|
||||
}
|
||||
|
||||
/// Returns a single value fetched from a fetch request.
|
||||
///
|
||||
/// The value is decoded from the leftmost column.
|
||||
///
|
||||
/// The result is nil if the request returns no row, or one row with a
|
||||
/// `NULL` value.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .select(Column("score"))
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName) LIMIT 1
|
||||
/// """
|
||||
///
|
||||
/// let scores = try Int.fetchOne(db, request)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - request: A FetchRequest.
|
||||
/// - returns: An optional value.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchOne(_ db: Database, _ request: some FetchRequest) throws -> Self? {
|
||||
let request = try request.makePreparedRequest(db, forSingleResult: true)
|
||||
return try fetchOne(request.statement, adapter: request.adapter)
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseValueConvertible where Self: StatementColumnConvertible & Hashable {
|
||||
/// Returns a set of values fetched from a fetch request.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .select(Column("score"))
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// let scores = try Int.fetchAll(db, request)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - db: A database connection.
|
||||
/// - request: A FetchRequest.
|
||||
/// - returns: A set of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public static func fetchSet(_ db: Database, _ request: some FetchRequest) throws -> Set<Self> {
|
||||
let request = try request.makePreparedRequest(db, forSingleResult: false)
|
||||
return try fetchSet(request.statement, adapter: request.adapter)
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementColumnConvertible {
|
||||
|
||||
// MARK: Fetching Values
|
||||
|
||||
/// Returns a cursor over fetched values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
/// .select(Column("score"), as: Int.self)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// let scores = try request.fetchCursor(db)
|
||||
/// while let score = try scores.next() {
|
||||
/// print(score)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column.
|
||||
///
|
||||
/// The returned cursor is valid only during the remaining execution of the
|
||||
/// database access. Do not store or return the cursor for later use.
|
||||
///
|
||||
/// If the database is modified during the cursor iteration, the remaining
|
||||
/// elements are undefined.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: A ``FastDatabaseValueCursor`` over fetched values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func fetchCursor(_ db: Database) throws -> FastDatabaseValueCursor<RowDecoder> {
|
||||
try RowDecoder.fetchCursor(db, self)
|
||||
}
|
||||
|
||||
/// Returns an array of fetched values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
/// .select(Column("score"), as: Int.self)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// let scores = try request.fetchAll(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: An array of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func fetchAll(_ db: Database) throws -> [RowDecoder] {
|
||||
try RowDecoder.fetchAll(db, self)
|
||||
}
|
||||
|
||||
/// Returns a single fetched value.
|
||||
///
|
||||
/// The value is decoded from the leftmost column.
|
||||
///
|
||||
/// The result is nil if the request returns no row, or one row with a
|
||||
/// `NULL` value.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
/// .select(Column("score"), as: Int.self)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName) LIMIT 1
|
||||
/// """
|
||||
///
|
||||
/// let score = try request.fetchOne(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: An optional value.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func fetchOne(_ db: Database) throws -> RowDecoder? {
|
||||
try RowDecoder.fetchOne(db, self)
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchRequest where RowDecoder: DatabaseValueConvertible & StatementColumnConvertible & Hashable {
|
||||
/// Returns a set of fetched values.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// try dbQueue.read { db in
|
||||
/// let lastName = "O'Reilly"
|
||||
///
|
||||
/// // Query interface request
|
||||
/// let request = Player
|
||||
/// .filter(Column("lastName") == lastName)
|
||||
/// .select(Column("score"), as: Int.self)
|
||||
///
|
||||
/// // SQL request
|
||||
/// let request: SQLRequest<Int> = """
|
||||
/// SELECT score FROM player WHERE lastName = \(lastName)
|
||||
/// """
|
||||
///
|
||||
/// let scores = try request.fetchSet(db)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Values are decoded from the leftmost column.
|
||||
///
|
||||
/// - parameter db: A database connection.
|
||||
/// - returns: A set of values.
|
||||
/// - throws: A ``DatabaseError`` whenever an SQLite error occurs.
|
||||
public func fetchSet(_ db: Database) throws -> Set<RowDecoder> {
|
||||
try RowDecoder.fetchSet(db, self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#if canImport(CoreGraphics)
|
||||
import CoreGraphics
|
||||
|
||||
/// CGFloat adopts DatabaseValueConvertible
|
||||
extension CGFloat: DatabaseValueConvertible {
|
||||
/// Returns a REAL database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Double(self).databaseValue
|
||||
}
|
||||
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> CGFloat? {
|
||||
guard let double = Double.fromDatabaseValue(dbValue) else {
|
||||
return nil
|
||||
}
|
||||
return CGFloat(double)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
|
||||
/// Data is convertible to and from DatabaseValue.
|
||||
extension Data: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
if let bytes = sqlite3_column_blob(sqliteStatement, index) {
|
||||
let count = Int(sqlite3_column_bytes(sqliteStatement, index))
|
||||
self.init(bytes: bytes, count: count) // copy bytes
|
||||
} else {
|
||||
self.init()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a BLOB database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
DatabaseValue(storage: .blob(self))
|
||||
}
|
||||
|
||||
/// Returns a `Data` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a data blob, returns it.
|
||||
///
|
||||
/// If the database value contains a string, returns this string converted
|
||||
/// to UTF8 data.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Data? {
|
||||
switch dbValue.storage {
|
||||
case .blob(let data):
|
||||
return data
|
||||
case .string(let string):
|
||||
// Implicit conversion from string to blob, just as SQLite does
|
||||
// See <https://www.sqlite.org/c3ref/column_blob.html>
|
||||
return string.data(using: .utf8)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
withUnsafeBytes {
|
||||
sqlite3_bind_blob(sqliteStatement, index, $0.baseAddress, CInt($0.count), SQLITE_TRANSIENT)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls the given closure after binding a statement argument.
|
||||
///
|
||||
/// The binding is valid only during the execution of this method.
|
||||
///
|
||||
/// - parameter sqliteStatement: An SQLite statement.
|
||||
/// - parameter index: 1-based index to statement arguments.
|
||||
/// - parameter body: The closure to execute when argument is bound.
|
||||
func withBinding<T>(to sqliteStatement: SQLiteStatement, at index: CInt, do body: () throws -> T) throws -> T {
|
||||
try withUnsafeBytes {
|
||||
let code = sqlite3_bind_blob(
|
||||
sqliteStatement, index,
|
||||
$0.baseAddress, CInt($0.count), nil /* SQLITE_STATIC */)
|
||||
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Conversions
|
||||
|
||||
extension Data {
|
||||
static func fastDecodeNoCopy(
|
||||
fromStatement sqliteStatement: SQLiteStatement,
|
||||
atUncheckedIndex index: CInt,
|
||||
context: @autoclosure () -> RowDecodingContext)
|
||||
throws -> Data
|
||||
{
|
||||
guard sqlite3_column_type(sqliteStatement, index) != SQLITE_NULL else {
|
||||
throw RowDecodingError.valueMismatch(
|
||||
Data.self,
|
||||
sqliteStatement: sqliteStatement,
|
||||
index: index,
|
||||
context: context())
|
||||
}
|
||||
guard let bytes = sqlite3_column_blob(sqliteStatement, index) else {
|
||||
return Data()
|
||||
}
|
||||
let count = Int(sqlite3_column_bytes(sqliteStatement, index))
|
||||
return Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: bytes), count: count, deallocator: .none)
|
||||
}
|
||||
}
|
||||
+171
@@ -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)!)
|
||||
}
|
||||
}
|
||||
+47
@@ -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
|
||||
+227
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+194
@@ -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) }
|
||||
}
|
||||
}
|
||||
+155
@@ -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
|
||||
}
|
||||
}
|
||||
+73
@@ -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) }
|
||||
}
|
||||
}
|
||||
+97
@@ -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)
|
||||
}
|
||||
}
|
||||
+927
@@ -0,0 +1,927 @@
|
||||
// MARK: - Value Types
|
||||
|
||||
/// Bool adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Bool: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
self = sqlite3_column_int64(sqliteStatement, index) != 0
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
(self ? 1 : 0).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `Bool` from the specified database value.
|
||||
///
|
||||
/// If the database value contains an integer or a double, returns whether
|
||||
/// this number is zero.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Bool? {
|
||||
// IMPLEMENTATION NOTE
|
||||
//
|
||||
// https://www.sqlite.org/lang_expr.html#booleanexpr
|
||||
//
|
||||
// > # Boolean Expressions
|
||||
// >
|
||||
// > The SQL language features several contexts where an expression is
|
||||
// > evaluated and the result converted to a boolean (true or false)
|
||||
// > value. These contexts are:
|
||||
// >
|
||||
// > - the WHERE clause of a SELECT, UPDATE or DELETE statement,
|
||||
// > - the ON or USING clause of a join in a SELECT statement,
|
||||
// > - the HAVING clause of a SELECT statement,
|
||||
// > - the WHEN clause of an SQL trigger, and
|
||||
// > - the WHEN clause or clauses of some CASE expressions.
|
||||
// >
|
||||
// > To convert the results of an SQL expression to a boolean value,
|
||||
// > SQLite first casts the result to a NUMERIC value in the same way as
|
||||
// > a CAST expression. A numeric zero value (integer value 0 or real
|
||||
// > value 0.0) is considered to be false. A NULL value is still NULL.
|
||||
// > All other values are considered true.
|
||||
// >
|
||||
// > For example, the values NULL, 0.0, 0, 'english' and '0' are all
|
||||
// > considered to be false. Values 1, 1.0, 0.1, -0.1 and '1english' are
|
||||
// > considered to be true.
|
||||
//
|
||||
// OK so we have to support boolean for all storage classes?
|
||||
// Actually we won't, because of the SQLite boolean interpretation of
|
||||
// strings:
|
||||
//
|
||||
// The doc says that "english" should be false, and "1english" should
|
||||
// be true. I guess "-1english" and "0.1english" should be true also.
|
||||
// And... what about "0.0e10english"?
|
||||
//
|
||||
// Ideally, we'd ask SQLite to perform the conversion itself, and return
|
||||
// its own boolean interpretation of the string. Unfortunately, it looks
|
||||
// like it is not so easy...
|
||||
//
|
||||
// So we could take a short route, and assume all strings are false,
|
||||
// since most strings are falsey for SQLite.
|
||||
//
|
||||
// Considering all strings falsey is unfortunately very
|
||||
// counter-intuitive. This is not the correct way to tackle the boolean
|
||||
// problem.
|
||||
//
|
||||
// Instead, let's use the fact that the BOOLEAN typename has Numeric
|
||||
// affinity (https://www.sqlite.org/datatype3.html), and that the doc
|
||||
// says:
|
||||
//
|
||||
// > SQLite does not have a separate Boolean storage class. Instead,
|
||||
// > Boolean values are stored as integers 0 (false) and 1 (true).
|
||||
//
|
||||
// So we extract bools from Integer and Real only. Integer because it is
|
||||
// the natural boolean storage class, and Real because Numeric affinity
|
||||
// store big numbers as Real.
|
||||
|
||||
switch dbValue.storage {
|
||||
case .int64(let int64):
|
||||
return (int64 != 0)
|
||||
case .double(let double):
|
||||
return (double != 0.0)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, self ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Int adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Int: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = Int(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `Int` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { Int(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Int8 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Int8: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = Int8(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `Int8` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int8? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { Int8(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Int16 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Int16: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = Int16(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `Int16` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int16? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { Int16(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Int32 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Int32: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = Int32(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `Int32` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int32? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { Int32(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Int64 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Int64: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
self = sqlite3_column_int64(sqliteStatement, index)
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
DatabaseValue(storage: .int64(self))
|
||||
}
|
||||
|
||||
/// Returns a `Int64` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer, returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Int64? {
|
||||
switch dbValue.storage {
|
||||
case .int64(let int64):
|
||||
return int64
|
||||
case .double(let double):
|
||||
guard double >= Double(Int64.min) else { return nil }
|
||||
guard double < Double(Int64.max) else { return nil }
|
||||
return Int64(double)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// UInt adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension UInt: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = UInt(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `UInt` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { UInt(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// UInt8 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension UInt8: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = UInt8(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `UInt8` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt8? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { UInt8(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// UInt16 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension UInt16: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = UInt16(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `UInt16` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt16? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { UInt16(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// UInt32 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension UInt32: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = UInt32(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `UInt32` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt32? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { UInt32(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// UInt64 adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension UInt64: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public init?(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
let int64 = sqlite3_column_int64(sqliteStatement, index)
|
||||
guard let v = UInt64(exactly: int64) else { return nil }
|
||||
self = v
|
||||
}
|
||||
|
||||
/// Returns an INTEGER database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Int64(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `UInt64` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer representable in this type,
|
||||
/// returns this integer.
|
||||
///
|
||||
/// If the database value contains a double representable in this type after
|
||||
/// rounding toward zero, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> UInt64? {
|
||||
Int64.fromDatabaseValue(dbValue).flatMap { UInt64(exactly: $0) }
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_int64(sqliteStatement, index, Int64(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Double adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Double: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
self = sqlite3_column_double(sqliteStatement, index)
|
||||
}
|
||||
|
||||
/// Returns a REAL database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
DatabaseValue(storage: .double(self))
|
||||
}
|
||||
|
||||
/// Returns a `Double` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer, returns the conversion.
|
||||
///
|
||||
/// If the database value contains a double, returns this double.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Double? {
|
||||
switch dbValue.storage {
|
||||
case .int64(let int64):
|
||||
return Double(int64)
|
||||
case .double(let double):
|
||||
return double
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_double(sqliteStatement, index, self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Float adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension Float: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
self = Float(sqlite3_column_double(sqliteStatement, index))
|
||||
}
|
||||
|
||||
/// Returns a REAL database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
Double(self).databaseValue
|
||||
}
|
||||
|
||||
/// Returns a `Float` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a integer, returns the conversion.
|
||||
///
|
||||
/// If the database value contains a double, returns the conversion.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Float? {
|
||||
switch dbValue.storage {
|
||||
case .int64(let int64):
|
||||
return Float(int64)
|
||||
case .double(let double):
|
||||
return Float(double)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_double(sqliteStatement, index, Double(self))
|
||||
}
|
||||
}
|
||||
|
||||
/// String adopts DatabaseValueConvertible and StatementColumnConvertible.
|
||||
extension String: DatabaseValueConvertible, StatementColumnConvertible {
|
||||
|
||||
/// Returns a value initialized from a raw SQLite statement pointer.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - sqliteStatement: A pointer to an SQLite statement.
|
||||
/// - index: The column index.
|
||||
public init(sqliteStatement: SQLiteStatement, index: CInt) {
|
||||
self = String(cString: sqlite3_column_text(sqliteStatement, index)!)
|
||||
}
|
||||
|
||||
/// Returns a TEXT database value.
|
||||
public var databaseValue: DatabaseValue {
|
||||
DatabaseValue(storage: .string(self))
|
||||
}
|
||||
|
||||
/// Returns a `String` from the specified database value.
|
||||
///
|
||||
/// If the database value contains a string, returns it.
|
||||
///
|
||||
/// If the database value contains a data blob, parses this data as an
|
||||
/// UTF8 string.
|
||||
///
|
||||
/// Otherwise, returns nil.
|
||||
public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> String? {
|
||||
switch dbValue.storage {
|
||||
case .blob(let data):
|
||||
// Implicit conversion from blob to string, just as SQLite does
|
||||
// See <https://www.sqlite.org/c3ref/column_blob.html>
|
||||
return String(data: data, encoding: .utf8)
|
||||
case .string(let string):
|
||||
return string
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func bind(to sqliteStatement: SQLiteStatement, at index: CInt) -> CInt {
|
||||
sqlite3_bind_text(sqliteStatement, index, self, -1, SQLITE_TRANSIENT)
|
||||
}
|
||||
|
||||
/// Calls the given closure after binding a statement argument.
|
||||
///
|
||||
/// The binding is valid only during the execution of this method.
|
||||
///
|
||||
/// - parameter sqliteStatement: An SQLite statement.
|
||||
/// - parameter index: 1-based index to statement arguments.
|
||||
/// - parameter body: The closure to execute when argument is bound.
|
||||
func withBinding<T>(to sqliteStatement: SQLiteStatement, at index: CInt, do body: () throws -> T) throws -> T {
|
||||
try withCString {
|
||||
let code = sqlite3_bind_text(sqliteStatement, index, $0, -1, nil /* SQLITE_STATIC */)
|
||||
try checkBindingSuccess(code: code, sqliteStatement: sqliteStatement)
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SQL Functions
|
||||
|
||||
extension DatabaseFunction {
|
||||
/// An SQL function that calls the Foundation
|
||||
/// `String.capitalized` property.
|
||||
///
|
||||
/// `NULL` is returned for non-strings values.
|
||||
///
|
||||
/// This function is automatically added by GRDB to your database
|
||||
/// connections. It is the function used by the query interface's
|
||||
/// ``SQLSpecificExpressible/capitalized``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let nameColumn = Column("name")
|
||||
/// let request = Player.select(nameColumn.capitalized)
|
||||
/// let names = try String.fetchAll(dbQueue, request) // [String]
|
||||
/// ```
|
||||
public static let capitalize =
|
||||
DatabaseFunction("swiftCapitalizedString", argumentCount: 1, pure: true) { dbValues in
|
||||
guard let string = String.fromDatabaseValue(dbValues[0]) else {
|
||||
return nil
|
||||
}
|
||||
return string.capitalized
|
||||
}
|
||||
|
||||
/// An SQL function that calls the Swift
|
||||
/// `String.lowercased()` method.
|
||||
///
|
||||
/// `NULL` is returned for non-strings values.
|
||||
///
|
||||
/// This function is automatically added by GRDB to your database
|
||||
/// connections. It is the function used by the query interface's
|
||||
/// ``SQLSpecificExpressible/lowercased``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let nameColumn = Column("name")
|
||||
/// let request = Player.select(nameColumn.lowercased)
|
||||
/// let names = try String.fetchAll(dbQueue, request) // [String]
|
||||
/// ```
|
||||
public static let lowercase =
|
||||
DatabaseFunction("swiftLowercaseString", argumentCount: 1, pure: true) { dbValues in
|
||||
guard let string = String.fromDatabaseValue(dbValues[0]) else {
|
||||
return nil
|
||||
}
|
||||
return string.lowercased()
|
||||
}
|
||||
|
||||
/// An SQL function that calls the Swift
|
||||
/// `String.uppercased()` method.
|
||||
///
|
||||
/// `NULL` is returned for non-strings values.
|
||||
///
|
||||
/// This function is automatically added by GRDB to your database
|
||||
/// connections. It is the function used by the query interface's
|
||||
/// ``SQLSpecificExpressible/uppercased``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let nameColumn = Column("name")
|
||||
/// let request = Player.select(nameColumn.uppercased)
|
||||
/// let names = try String.fetchAll(dbQueue, request) // [String]
|
||||
/// ```
|
||||
public static let uppercase =
|
||||
DatabaseFunction("swiftUppercaseString", argumentCount: 1, pure: true) { dbValues in
|
||||
guard let string = String.fromDatabaseValue(dbValues[0]) else {
|
||||
return nil
|
||||
}
|
||||
return string.uppercased()
|
||||
}
|
||||
|
||||
/// An SQL function that calls the Foundation
|
||||
/// `String.localizedCapitalized` property.
|
||||
///
|
||||
/// `NULL` is returned for non-strings values.
|
||||
///
|
||||
/// This function is automatically added by GRDB to your database
|
||||
/// connections. It is the function used by the query interface's
|
||||
/// ``SQLSpecificExpressible/localizedCapitalized``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let nameColumn = Column("name")
|
||||
/// let request = Player.select(nameColumn.localizedCapitalized)
|
||||
/// let names = try String.fetchAll(dbQueue, request) // [String]
|
||||
/// ```
|
||||
public static let localizedCapitalize =
|
||||
DatabaseFunction("swiftLocalizedCapitalizedString", argumentCount: 1, pure: true) { dbValues in
|
||||
guard let string = String.fromDatabaseValue(dbValues[0]) else {
|
||||
return nil
|
||||
}
|
||||
return string.localizedCapitalized
|
||||
}
|
||||
|
||||
|
||||
/// An SQL function that calls the Foundation
|
||||
/// `String.localizedLowercase` property.
|
||||
///
|
||||
/// `NULL` is returned for non-strings values.
|
||||
///
|
||||
/// This function is automatically added by GRDB to your database
|
||||
/// connections. It is the function used by the query interface's
|
||||
/// ``SQLSpecificExpressible/localizedLowercased``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let nameColumn = Column("name")
|
||||
/// let request = Player.select(nameColumn.localizedLowercase)
|
||||
/// let names = try String.fetchAll(dbQueue, request) // [String]
|
||||
/// ```
|
||||
public static let localizedLowercase =
|
||||
DatabaseFunction("swiftLocalizedLowercaseString", argumentCount: 1, pure: true) { dbValues in
|
||||
guard let string = String.fromDatabaseValue(dbValues[0]) else {
|
||||
return nil
|
||||
}
|
||||
return string.localizedLowercase
|
||||
}
|
||||
|
||||
/// An SQL function that calls the Foundation
|
||||
/// `String.localizedUppercase` property.
|
||||
///
|
||||
/// `NULL` is returned for non-strings values.
|
||||
///
|
||||
/// This function is automatically added by GRDB to your database
|
||||
/// connections. It is the function used by the query interface's
|
||||
/// ``SQLSpecificExpressible/localizedUppercased``:
|
||||
///
|
||||
/// ```swift
|
||||
/// let nameColumn = Column("name")
|
||||
/// let request = Player.select(nameColumn.localizedUppercase)
|
||||
/// let names = try String.fetchAll(dbQueue, request) // [String]
|
||||
/// ```
|
||||
public static let localizedUppercase =
|
||||
DatabaseFunction("swiftLocalizedUppercaseString", argumentCount: 1, pure: true) { dbValues in
|
||||
guard let string = String.fromDatabaseValue(dbValues[0]) else {
|
||||
return nil
|
||||
}
|
||||
return string.localizedUppercase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - SQLite Collations
|
||||
|
||||
extension DatabaseCollation {
|
||||
// Here we define a set of predefined collations.
|
||||
//
|
||||
// We should avoid renaming those collations, because database created with
|
||||
// earlier versions of the library may have used those collations in the
|
||||
// definition of tables. A renaming would prevent SQLite to find the
|
||||
// collation.
|
||||
//
|
||||
// Yet we're not absolutely stuck: we could register support for obsolete
|
||||
// collation names with sqlite3_collation_needed().
|
||||
// See https://www.sqlite.org/capi3ref.html#sqlite3_collation_needed
|
||||
|
||||
/// A collation that compares strings according to the built-in `==` and
|
||||
/// `<=` operators of the Swift String.
|
||||
///
|
||||
/// This collation is automatically added by GRDB to your database
|
||||
/// connections.
|
||||
///
|
||||
/// You can use the collation when creating database tables:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "player") { t in
|
||||
/// t.column("name", .text).collate(.unicodeCompare)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Embed the collation name in your raw SQL queries:
|
||||
///
|
||||
/// ```swift
|
||||
/// let collationName = DatabaseCollation.unicodeCompare.name
|
||||
/// dbQueue.execute(sql: """
|
||||
/// CREATE TABLE player (
|
||||
/// name TEXT COLLATE \(collationName)
|
||||
/// )
|
||||
/// """)
|
||||
/// ```
|
||||
public static let unicodeCompare =
|
||||
DatabaseCollation("swiftCompare") { (lhs, rhs) in
|
||||
(lhs < rhs) ? .orderedAscending : ((lhs == rhs) ? .orderedSame : .orderedDescending)
|
||||
}
|
||||
|
||||
/// A collation that compares strings according to the Foundation
|
||||
/// `String.caseInsensitiveCompare(_:)` method.
|
||||
///
|
||||
/// This collation is automatically added by GRDB to your database
|
||||
/// connections.
|
||||
///
|
||||
/// You can use the collation when creating database tables:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "player") { t in
|
||||
/// t.column("name", .text).collate(.caseInsensitiveCompare)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Embed the collation name in your raw SQL queries:
|
||||
///
|
||||
/// ```swift
|
||||
/// let collationName = DatabaseCollation.caseInsensitiveCompare.name
|
||||
/// dbQueue.execute(sql: """
|
||||
/// CREATE TABLE player (
|
||||
/// name TEXT COLLATE \(collationName)
|
||||
/// )
|
||||
/// """)
|
||||
/// ```
|
||||
public static let caseInsensitiveCompare =
|
||||
DatabaseCollation("swiftCaseInsensitiveCompare") { (lhs, rhs) in
|
||||
lhs.caseInsensitiveCompare(rhs)
|
||||
}
|
||||
|
||||
/// A collation that compares strings according to the Foundation
|
||||
/// `String.localizedCaseInsensitiveCompare(_:)` method.
|
||||
///
|
||||
/// This collation is automatically added by GRDB to your database
|
||||
/// connections.
|
||||
///
|
||||
/// You can use the collation when creating database tables:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "player") { t in
|
||||
/// t.column("name", .text).collate(.localizedCaseInsensitiveCompare)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Embed the collation name in your raw SQL queries:
|
||||
///
|
||||
/// ```swift
|
||||
/// let collationName = DatabaseCollation.localizedCaseInsensitiveCompare.name
|
||||
/// dbQueue.execute(sql: """
|
||||
/// CREATE TABLE player (
|
||||
/// name TEXT COLLATE \(collationName)
|
||||
/// )
|
||||
/// """)
|
||||
/// ```
|
||||
public static let localizedCaseInsensitiveCompare =
|
||||
DatabaseCollation("swiftLocalizedCaseInsensitiveCompare") { (lhs, rhs) in
|
||||
lhs.localizedCaseInsensitiveCompare(rhs)
|
||||
}
|
||||
|
||||
/// A collation that compares strings according to the Foundation
|
||||
/// `String.localizedCompare(_:)` method.
|
||||
///
|
||||
/// This collation is automatically added by GRDB to your database
|
||||
/// connections.
|
||||
///
|
||||
/// You can use the collation when creating database tables:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "player") { t in
|
||||
/// t.column("name", .text).collate(.localizedCompare)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Embed the collation name in your raw SQL queries:
|
||||
///
|
||||
/// ```swift
|
||||
/// let collationName = DatabaseCollation.localizedCompare.name
|
||||
/// dbQueue.execute(sql: """
|
||||
/// CREATE TABLE player (
|
||||
/// name TEXT COLLATE \(collationName)
|
||||
/// )
|
||||
/// """)
|
||||
/// ```
|
||||
public static let localizedCompare =
|
||||
DatabaseCollation("swiftLocalizedCompare") { (lhs, rhs) in
|
||||
lhs.localizedCompare(rhs)
|
||||
}
|
||||
|
||||
/// A collation that compares strings according to the Foundation
|
||||
/// `String.localizedStandardCompare(_:)` method.
|
||||
///
|
||||
/// This collation is automatically added by GRDB to your database
|
||||
/// connections.
|
||||
///
|
||||
/// You can use the collation when creating database tables:
|
||||
///
|
||||
/// ```swift
|
||||
/// try db.create(table: "player") { t in
|
||||
/// t.column("name", .text).collate(.localizedStandardCompare)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Embed the collation name in your raw SQL queries:
|
||||
///
|
||||
/// ```swift
|
||||
/// let collationName = DatabaseCollation.localizedStandardCompare.name
|
||||
/// dbQueue.execute(sql: """
|
||||
/// CREATE TABLE player (
|
||||
/// name TEXT COLLATE \(collationName)
|
||||
/// )
|
||||
/// """)
|
||||
/// ```
|
||||
public static let localizedStandardCompare =
|
||||
DatabaseCollation("swiftLocalizedStandardCompare") { (lhs, rhs) in
|
||||
lhs.localizedStandardCompare(rhs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
|
||||
/// A type that provides the moment of a transaction.
|
||||
///
|
||||
/// - note: [**🔥 EXPERIMENTAL**](https://github.com/groue/GRDB.swift/blob/master/README.md#what-are-experimental-features)
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Built-in Clocks
|
||||
///
|
||||
/// - ``DefaultTransactionClock``
|
||||
/// - ``CustomTransactionClock``
|
||||
public protocol TransactionClock {
|
||||
/// Returns the date of the current transaction.
|
||||
///
|
||||
/// This function is called whenever a transaction starts - precisely
|
||||
/// speaking, whenever the database connection leaves the auto-commit mode.
|
||||
///
|
||||
/// It is also called when the ``Database/transactionDate`` property is
|
||||
/// called, and the database connection is not in a transaction.
|
||||
///
|
||||
/// Related SQLite documentation: <https://www.sqlite.org/c3ref/get_autocommit.html>
|
||||
func now(_ db: Database) throws -> Date
|
||||
}
|
||||
|
||||
extension TransactionClock where Self == DefaultTransactionClock {
|
||||
/// Returns the default clock.
|
||||
public static var `default`: Self { DefaultTransactionClock() }
|
||||
}
|
||||
|
||||
extension TransactionClock where Self == CustomTransactionClock {
|
||||
/// Returns a custom clock.
|
||||
///
|
||||
/// The provided closure is called whenever a transaction starts - precisely
|
||||
/// speaking, whenever the database connection leaves the auto-commit mode.
|
||||
///
|
||||
/// It is also called when the ``Database/transactionDate`` property is
|
||||
/// called, and the database connection is not in a transaction.
|
||||
public static func custom(_ now: @escaping (Database) throws -> Date) -> Self {
|
||||
CustomTransactionClock(now)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default transaction clock.
|
||||
public struct DefaultTransactionClock: TransactionClock {
|
||||
/// Returns the start date of the current transaction.
|
||||
public func now(_ db: Database) throws -> Date {
|
||||
// An opportunity to fetch transaction time from the database when
|
||||
// SQLite supports the feature.
|
||||
Date()
|
||||
}
|
||||
}
|
||||
|
||||
/// A custom transaction clock.
|
||||
public struct CustomTransactionClock: TransactionClock {
|
||||
let _now: (Database) throws -> Date
|
||||
|
||||
public init(_ now: @escaping (Database) throws -> Date) {
|
||||
self._now = now
|
||||
}
|
||||
|
||||
public func now(_ db: Database) throws -> Date {
|
||||
try _now(db)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// An instance of WALSnapshot records the state of a WAL mode database for some
|
||||
/// specific point in history.
|
||||
///
|
||||
/// We use `WALSnapshot` to help `ValueObservation` check for changes
|
||||
/// that would happen between the initial fetch, and the start of the
|
||||
/// actual observation. This class has no other purpose, and is not intended to
|
||||
/// become public.
|
||||
///
|
||||
/// It does not work with SQLCipher, because SQLCipher does not support
|
||||
/// `SQLITE_ENABLE_SNAPSHOT` correctly: we have linker errors.
|
||||
/// See <https://github.com/ericsink/SQLitePCL.raw/issues/452>.
|
||||
///
|
||||
/// With custom SQLite builds, it only works if `SQLITE_ENABLE_SNAPSHOT`
|
||||
/// is defined.
|
||||
///
|
||||
/// With system SQLite, it can only work when the SDK exposes the C apis and
|
||||
/// their availability, which means XCode 14 (identified with Swift 5.7).
|
||||
///
|
||||
/// Yes, this is an awfully complex logic.
|
||||
///
|
||||
/// See <https://www.sqlite.org/c3ref/snapshot.html>.
|
||||
final class WALSnapshot: @unchecked Sendable {
|
||||
// @unchecked because sqlite3_snapshot has no threading requirements.
|
||||
// <https://www.sqlite.org/c3ref/snapshot.html>
|
||||
let sqliteSnapshot: UnsafeMutablePointer<sqlite3_snapshot>
|
||||
|
||||
init(_ db: Database) throws {
|
||||
var sqliteSnapshot: UnsafeMutablePointer<sqlite3_snapshot>?
|
||||
let code = withUnsafeMutablePointer(to: &sqliteSnapshot) {
|
||||
return sqlite3_snapshot_get(db.sqliteConnection, "main", $0)
|
||||
}
|
||||
guard code == SQLITE_OK else {
|
||||
// <https://www.sqlite.org/c3ref/snapshot_get.html>
|
||||
//
|
||||
// > The following must be true for sqlite3_snapshot_get() to succeed. [...]
|
||||
// >
|
||||
// > 1. The database handle must not be in autocommit mode.
|
||||
// > 2. Schema S of database connection D must be a WAL
|
||||
// > mode database.
|
||||
// > 3. There must not be a write transaction open on schema S
|
||||
// > of database connection D.
|
||||
// > 4. One or more transactions must have been written to the
|
||||
// > current wal file since it was created on disk (by any
|
||||
// > connection). This means that a snapshot cannot be taken
|
||||
// > on a wal mode database with no wal file immediately
|
||||
// > after it is first opened. At least one transaction must
|
||||
// > be written to it first.
|
||||
|
||||
// Test condition 1:
|
||||
if sqlite3_get_autocommit(db.sqliteConnection) != 0 {
|
||||
throw DatabaseError(resultCode: code, message: """
|
||||
Can't create snapshot because database is in autocommit mode.
|
||||
""")
|
||||
}
|
||||
|
||||
// Test condition 2:
|
||||
if let journalMode = try? String.fetchOne(db, sql: "PRAGMA journal_mode"),
|
||||
journalMode != "wal"
|
||||
{
|
||||
throw DatabaseError(resultCode: code, message: """
|
||||
Can't create snapshot because database is not in WAL mode.
|
||||
""")
|
||||
}
|
||||
|
||||
// Condition 3 can't happen because GRDB only calls this
|
||||
// initializer from read transactions.
|
||||
//
|
||||
// Hence it is condition 4 that is false:
|
||||
throw DatabaseError(resultCode: code, message: """
|
||||
Can't create snapshot from a missing or empty wal file.
|
||||
""")
|
||||
}
|
||||
guard let sqliteSnapshot else {
|
||||
throw DatabaseError(resultCode: .SQLITE_INTERNAL) // WTF SQLite?
|
||||
}
|
||||
self.sqliteSnapshot = sqliteSnapshot
|
||||
}
|
||||
|
||||
deinit {
|
||||
sqlite3_snapshot_free(sqliteSnapshot)
|
||||
}
|
||||
|
||||
/// Compares two WAL snapshots.
|
||||
///
|
||||
/// `a.compare(b) < 0` iff a is older than b.
|
||||
///
|
||||
/// See <https://www.sqlite.org/c3ref/snapshot_cmp.html>.
|
||||
func compare(_ other: WALSnapshot) -> CInt {
|
||||
sqlite3_snapshot_cmp(sqliteSnapshot, other.sqliteSnapshot)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,94 @@
|
||||
// swiftlint:disable:next line_length
|
||||
#if SQLITE_ENABLE_SNAPSHOT || (!GRDBCUSTOMSQLITE && !GRDBCIPHER && (compiler(>=5.7.1) || !(os(macOS) || targetEnvironment(macCatalyst))))
|
||||
/// A long-live read-only WAL transaction.
|
||||
///
|
||||
/// `WALSnapshotTransaction` **takes ownership** of its reader
|
||||
/// `SerializedDatabase` (TODO: make it a move-only type eventually).
|
||||
class WALSnapshotTransaction {
|
||||
private let reader: SerializedDatabase
|
||||
private let release: (_ isInsideTransaction: Bool) -> Void
|
||||
|
||||
/// The state of the database at the beginning of the transaction.
|
||||
let walSnapshot: WALSnapshot
|
||||
|
||||
/// Creates a long-live WAL transaction on a read-only connection.
|
||||
///
|
||||
/// The `release` closure is always called. It is called when the
|
||||
/// `WALSnapshotTransaction` is deallocated, or if the initializer
|
||||
/// throws.
|
||||
///
|
||||
/// In normal operations, the argument to `release` is always false,
|
||||
/// meaning that the connection is no longer in a transaction. If true,
|
||||
/// the connection has been left inside a transaction, due to
|
||||
/// some error.
|
||||
///
|
||||
/// Usage:
|
||||
///
|
||||
/// ```swift
|
||||
/// let transaction = WALSnapshotTransaction(
|
||||
/// reader: reader,
|
||||
/// release: { isInsideTransaction in
|
||||
/// ...
|
||||
/// })
|
||||
/// ```
|
||||
///
|
||||
/// - parameter reader: A read-only database connection.
|
||||
/// - parameter release: A closure to call when the read-only connection
|
||||
/// is no longer used.
|
||||
init(
|
||||
onReader reader: SerializedDatabase,
|
||||
release: @escaping (_ isInsideTransaction: Bool) -> Void)
|
||||
throws
|
||||
{
|
||||
assert(reader.configuration.readonly)
|
||||
|
||||
do {
|
||||
// Open a long-lived transaction, and enter snapshot isolation
|
||||
self.walSnapshot = try reader.sync(allowingLongLivedTransaction: true) { db in
|
||||
try db.beginTransaction(.deferred)
|
||||
// This also acquires snapshot isolation because checking
|
||||
// database schema performs a read access.
|
||||
try db.clearSchemaCacheIfNeeded()
|
||||
return try WALSnapshot(db)
|
||||
}
|
||||
self.reader = reader
|
||||
self.release = release
|
||||
} catch {
|
||||
// self is not initialized, so deinit will not run.
|
||||
Self.commitAndRelease(reader: reader, release: release)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
Self.commitAndRelease(reader: reader, release: release)
|
||||
}
|
||||
|
||||
/// Executes database operations in the snapshot transaction, and
|
||||
/// returns their result after they have finished executing.
|
||||
func read<T>(_ value: (Database) throws -> T) rethrows -> T {
|
||||
// We should check the validity of the snapshot, as DatabaseSnapshotPool does.
|
||||
try reader.sync(value)
|
||||
}
|
||||
|
||||
/// Schedules database operations for execution, and
|
||||
/// returns immediately.
|
||||
func asyncRead(_ value: @escaping (Database) -> Void) {
|
||||
// We should check the validity of the snapshot, as DatabaseSnapshotPool does.
|
||||
reader.async(value)
|
||||
}
|
||||
|
||||
private static func commitAndRelease(
|
||||
reader: SerializedDatabase,
|
||||
release: (_ isInsideTransaction: Bool) -> Void)
|
||||
{
|
||||
// WALSnapshotTransaction may be deinitialized in the dispatch
|
||||
// queue of its reader: allow reentrancy.
|
||||
let isInsideTransaction = reader.reentrantSync(allowingLongLivedTransaction: false) { db in
|
||||
try? db.commit()
|
||||
return db.isInsideTransaction
|
||||
}
|
||||
release(isInsideTransaction)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user