This commit is contained in:
zeus
2025-01-28 12:28:03 +08:00
parent 738c373a77
commit ec96756800
2534 changed files with 486292 additions and 0 deletions
@@ -0,0 +1,109 @@
# ``GRDB/Configuration``
The configuration of a database connection.
## Overview
You create a `Configuration` before opening a database connection:
```swift
var config = Configuration()
config.readonly = true
config.maximumReaderCount = 2 // (DatabasePool only) The default is 5
let dbQueue = try DatabaseQueue( // or DatabasePool
path: "/path/to/database.sqlite",
configuration: config)
```
See <doc:DatabaseConnections>.
## Frequent Use Cases
#### Tracing SQL Statements
You can setup a tracing function that prints out all executed SQL requests with ``prepareDatabase(_:)`` and ``Database/trace(options:_:)``:
```swift
var config = Configuration()
config.prepareDatabase { db in
db.trace { print("SQL> \($0)") }
}
let dbQueue = try DatabaseQueue(
path: "/path/to/database.sqlite",
configuration: config)
// Prints "SQL> SELECT COUNT(*) FROM player"
let playerCount = dbQueue.read { db in
try Player.fetchCount(db)
}
```
#### Public Statement Arguments
Debugging is easier when database errors and tracing functions expose the values sent to the database. Since those values may contain sensitive information, verbose logging is disabled by default. You turn it on with ``publicStatementArguments``:
```swift
var config = Configuration()
#if DEBUG
// Protect sensitive information by enabling
// verbose debugging in DEBUG builds only.
config.publicStatementArguments = true
#endif
let dbQueue = try DatabaseQueue(
path: "/path/to/database.sqlite",
configuration: config)
do {
try dbQueue.write { db in
user.name = ...
user.location = ...
user.address = ...
user.phoneNumber = ...
try user.save(db)
}
} catch {
// Prints sensitive information in debug builds only
print(error)
}
```
> Warning: It is your responsibility to prevent sensitive information from leaking in unexpected locations, so you should not set the `publicStatementArguments` flag in release builds (think about GDPR and other privacy-related rules).
## Topics
### Creating a Configuration
- ``init()``
### Configuring SQLite Connections
- ``acceptsDoubleQuotedStringLiterals``
- ``busyMode``
- ``foreignKeysEnabled``
- ``journalMode``
- ``readonly``
- ``JournalModeConfiguration``
### Configuring GRDB Connections
- ``allowsUnsafeTransactions``
- ``defaultTransactionKind``
- ``label``
- ``maximumReaderCount``
- ``observesSuspensionNotifications``
- ``persistentReadOnlyConnections``
- ``prepareDatabase(_:)``
- ``publicStatementArguments``
- ``transactionClock``
- ``TransactionClock``
### Configuring the Quality of Service
- ``qos``
- ``readQoS``
- ``writeQoS``
- ``targetQueue``
- ``writeTargetQueue``
@@ -0,0 +1,95 @@
# ``GRDB/DatabasePool``
A database connection that allows concurrent accesses to an SQLite database.
## Usage
Open a `DatabasePool` with the path to a database file:
```swift
import GRDB
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")
```
SQLite creates the database file if it does not already exist. The connection is closed when the database queue gets deallocated.
**A `DatabasePool` can be used from any thread.** The ``DatabaseWriter/write(_:)-76inz`` and ``DatabaseReader/read(_:)-3806d`` methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue:
```swift
// Modify the database:
try dbPool.write { db in
try Player(name: "Arthur").insert(db)
}
// Read values:
try dbPool.read { db in
let players = try Player.fetchAll(db)
let playerCount = try Player.fetchCount(db)
}
```
Database access methods can return values:
```swift
let playerCount = try dbPool.read { db in
try Place.fetchCount(db)
}
let newPlayerCount = try dbPool.write { db -> Int in
try Player(name: "Arthur").insert(db)
return try Player.fetchCount(db)
}
```
The ``DatabaseWriter/write(_:)-76inz`` method wraps your database statements in a transaction that commits if and only if no error occurs. On the first unhandled error, all changes are reverted, the whole transaction is rollbacked, and the error is rethrown.
When you don't need to modify the database, prefer the ``DatabaseReader/read(_:)-3806d`` method, because several threads can perform reads in parallel.
When precise transaction handling is required, see <doc:Transactions>.
Asynchronous database accesses are described in <doc:Concurrency>.
`DatabasePool` can take snapshots of the database: see ``DatabaseSnapshot`` and ``DatabaseSnapshotPool``.
`DatabasePool` can be configured with ``Configuration``.
## Concurrency
A `DatabasePool` creates one writer SQLite connection, and a pool of read-only SQLite connections.
Unless ``Configuration/readonly``, the database is set to the [WAL mode](https://sqlite.org/wal.html). The WAL mode makes it possible for reads and writes to proceed concurrently.
All write accesses are executed in a serial **writer dispatch queue**, which means that there is never more than one thread that writes in the database.
All read accesses are executed in **reader dispatch queues** (one per read-only SQLite connection). Reads are generally non-blocking, unless the maximum number of concurrent reads has been reached. In this case, a read has to wait for another read to complete. That maximum number can be configured with ``Configuration/maximumReaderCount``.
SQLite connections are closed when the `DatabasePool` is deallocated.
`DatabasePool` inherits most of its database access methods from the ``DatabaseReader`` and ``DatabaseWriter`` protocols. It defines a few specific database access methods as well, listed below.
A `DatabasePool` needs your application to follow rules in order to deliver its safety guarantees. See <doc:Concurrency> for more information.
## Topics
### Creating a DatabasePool
- ``init(path:configuration:)``
### Accessing the Database
See ``DatabaseReader`` and ``DatabaseWriter`` for more database access methods.
- ``asyncConcurrentRead(_:)``
- ``writeInTransaction(_:_:)``
### Creating Database Snapshots
- ``makeSnapshot()``
- ``makeSnapshotPool()``
### Managing SQLite Connections
- ``invalidateReadOnlyConnections()``
- ``releaseMemory()``
- ``releaseMemoryEventually()``
@@ -0,0 +1,103 @@
# ``GRDB/DatabaseQueue``
A database connection that serializes accesses to an SQLite database.
## Usage
Open a `DatabaseQueue` with the path to a database file:
```swift
import GRDB
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
```
SQLite creates the database file if it does not already exist. The connection is closed when the database queue gets deallocated.
**A `DatabaseQueue` can be used from any thread.** The ``DatabaseWriter/write(_:)-76inz`` and ``DatabaseReader/read(_:)-3806d`` methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue:
```swift
// Modify the database:
try dbQueue.write { db in
try Player(name: "Arthur").insert(db)
}
// Read values:
try dbQueue.read { db in
let players = try Player.fetchAll(db)
let playerCount = try Player.fetchCount(db)
}
```
Database access methods can return values:
```swift
let playerCount = try dbQueue.read { db in
try Place.fetchCount(db)
}
let newPlayerCount = try dbQueue.write { db -> Int in
try Player(name: "Arthur").insert(db)
return try Player.fetchCount(db)
}
```
The ``DatabaseWriter/write(_:)-76inz`` method wraps your database statements in a transaction that commits if and only if no error occurs. On the first unhandled error, all changes are reverted, the whole transaction is rollbacked, and the error is rethrown.
When you don't need to modify the database, prefer the ``DatabaseReader/read(_:)-3806d`` method: it prevents any modification to the database.
When precise transaction handling is required, see <doc:Transactions>.
Asynchronous database accesses are described in <doc:Concurrency>.
`DatabaseQueue` can be configured with ``Configuration``.
## In-Memory Databases
`DatabaseQueue` can open a connection to an [in-memory SQLite database](https://www.sqlite.org/inmemorydb.html).
Such connections are quite handy for tests and SwiftUI previews, since you do not have to perform any cleanup of the file system.
```swift
let dbQueue = try DatabaseQueue()
```
In order to create several connections to the same in-memory database, give this database a name:
```swift
// A shared in-memory database
let dbQueue1 = try DatabaseQueue(named: "myDatabase")
// Another connection to the same database
let dbQueue2 = try DatabaseQueue(named: "myDatabase")
```
See ``init(named:configuration:)``.
## Concurrency
A `DatabaseQueue` creates one single SQLite connection. All database accesses are executed in a serial **writer dispatch queue**, which means that there is never more than one thread that uses the database. The SQLite connection is closed when the `DatabaseQueue` is deallocated.
`DatabaseQueue` inherits most of its database access methods from the ``DatabaseReader`` and ``DatabaseWriter`` protocols. It defines a few specific database access methods as well, listed below.
A `DatabaseQueue` needs your application to follow rules in order to deliver its safety guarantees. See <doc:Concurrency> for more information.
## Topics
### Creating a DatabaseQueue
- ``init(named:configuration:)``
- ``init(path:configuration:)``
- ``inMemoryCopy(fromPath:configuration:)``
- ``temporaryCopy(fromPath:configuration:)``
### Accessing the Database
See ``DatabaseReader`` and ``DatabaseWriter`` for more database access methods.
- ``inDatabase(_:)``
- ``inTransaction(_:_:)``
### Managing the SQLite Connection
- ``releaseMemory()``
@@ -0,0 +1,111 @@
# ``GRDB/DatabaseRegionObservation``
`DatabaseRegionObservation` tracks changes in a database region, and notifies impactful transactions.
## Overview
`DatabaseRegionObservation` tracks insertions, updates, and deletions that impact the tracked region, whether performed with raw SQL, or <doc:QueryInterface>. This includes indirect changes triggered by [foreign keys actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
See <doc:GRDB/DatabaseRegionObservation#Dealing-with-Undetected-Changes> below for the list of exceptions.
`DatabaseRegionObservation` calls your application right after changes have been committed in the database, and before any other thread had any opportunity to perform further changes. *This is a pretty strong guarantee, that most applications do not really need.* Instead, most applications prefer to be notified with fresh values: make sure you check ``ValueObservation`` before using `DatabaseRegionObservation`.
## DatabaseRegionObservation Usage
Create a `DatabaseRegionObservation` with one or several requests to track:
```swift
// Tracks the full player table
let observation = DatabaseRegionObservation(tracking: Player.all())
```
Then start the observation from a ``DatabaseQueue`` or ``DatabasePool``:
```swift
let cancellable = try observation.start(in: dbQueue) { error in
// Handle error
} onChange: { (db: Database) in
print("Players were changed")
}
```
Enjoy the changes notifications:
```swift
try dbQueue.write { db in
try Player(name: "Arthur").insert(db)
}
// Prints "Players were changed"
```
You stop the observation by calling the ``DatabaseCancellable/cancel()`` method on the object returned by the `start` method. Cancellation is automatic when the cancellable is deallocated:
```swift
cancellable.cancel()
```
`DatabaseRegionObservation` can also be turned into a Combine publisher, or an RxSwift observable (see the companion library [RxGRDB](https://github.com/RxSwiftCommunity/RxGRDB)):
```swift
let cancellable = observation.publisher(in: dbQueue).sink { completion in
// Handle completion
} receiveValue: { (db: Database) in
print("Players were changed")
}
```
You can feed `DatabaseRegionObservation` with any type that conforms to the ``DatabaseRegionConvertible`` protocol: ``FetchRequest``, ``DatabaseRegion``, ``Table``, etc. For example:
```swift
// Observe the score column of the 'player' table
let observation = DatabaseRegionObservation(
tracking: Player.select(Column("score")))
// Observe the 'score' column of the 'player' table
let observation = DatabaseRegionObservation(
tracking: SQLRequest("SELECT score FROM player"))
// Observe both the 'player' and 'team' tables
let observation = DatabaseRegionObservation(
tracking: Table("player"), Table("team"))
// Observe the full database
let observation = DatabaseRegionObservation(
tracking: .fullDatabase)
```
## Dealing with Undetected Changes
`DatabaseRegionObservation` will not notify impactful transactions whenever the database is modified in an undetectable way:
- Changes performed by external database connections.
- Changes performed by SQLite statements that are not compiled and executed by GRDB.
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
To have observations notify such undetected changes, applications can take explicit action: call the ``Database/notifyChanges(in:)`` `Database` method from a write transaction:
```swift
try dbQueue.write { db in
// Notify observations that some changes were performed in the database
try db.notifyChanges(in: .fullDatabase)
// Notify observations that some changes were performed in the player table
try db.notifyChanges(in: Player.all())
// Equivalent alternative
try db.notifyChanges(in: Table("player"))
}
```
## Topics
### Creating DatabaseRegionObservation
- ``init(tracking:)-5ldbe``
- ``init(tracking:)-2nqjd``
### Observing Database Transactions
- ``publisher(in:)``
- ``start(in:onError:onChange:)``
@@ -0,0 +1,183 @@
# ``GRDB/DatabaseValueConvertible``
A type that can convert itself into and out of a database value.
## Overview
A `DatabaseValueConvertible` type supports conversion to and from database values (null, integers, doubles, strings, and blobs). `DatabaseValueConvertible` is adopted by `Bool`, `Int`, `String`, `Date`, etc.
> Note: Types that converts to and from multiple columns in a database row must not conform to the `DatabaseValueConvertible` protocol. Those types are called **record types**, and should conform to record protocols instead. See <doc:QueryInterface>.
> Note: Standard collections `Array`, `Set`, and `Dictionary` do not conform to `DatabaseValueConvertible`. To store arrays, sets, or dictionaries in individual database values, wrap them as properties of `Codable` record types. They will automatically be stored as JSON objects and arrays. See <doc:QueryInterface>.
## Conforming to the DatabaseValueConvertible Protocol
To conform to `DatabaseValueConvertible`, implement the two requirements ``fromDatabaseValue(_:)-21zzv`` and ``databaseValue-1ob9k``. Do not customize the ``fromMissingColumn()-7iamp`` requirement. If your type `MyValue` conforms, then the conformance of the optional type `MyValue?` is automatic.
The implementation of `fromDatabaseValue` must return nil if the type can not be decoded from the raw database value. This nil value will have GRDB throw a decoding error accordingly.
For example:
```swift
struct EvenInteger {
let value: Int // Guaranteed even
init?(_ value: Int) {
guard value.isMultiple(of: 2) else {
return nil // Not an even number
}
self.value = value
}
}
extension EvenInteger: DatabaseValueConvertible {
var databaseValue: DatabaseValue {
value.databaseValue
}
static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
guard let value = Int.fromDatabaseValue(dbValue) else {
return nil // Not an integer
}
return EvenInteger(value) // Nil if not even
}
}
```
### Built-in RawRepresentable support
`DatabaseValueConvertible` implementation is ready-made for `RawRepresentable` types whose raw value is itself `DatabaseValueConvertible`, such as enums:
```swift
enum Grape: String {
case chardonnay, merlot, riesling
}
// Encodes and decodes `Grape` as a string in the database:
extension Grape: DatabaseValueConvertible { }
```
### Built-in Codable support
`DatabaseValueConvertible` is also ready-made for `Codable` types, which are automatically coded and decoded from JSON arrays and objects:
```swift
struct Color: Codable {
var red: Double
var green: Double
var blue: Double
}
// Encodes and decodes `Color` as a JSON object in the database:
extension Color: DatabaseValueConvertible { }
```
By default, such codable value types are encoded and decoded with the standard [JSONEncoder](https://developer.apple.com/documentation/foundation/jsonencoder) and [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder). `Data` values are handled with the `.base64` strategy, `Date` with the `.millisecondsSince1970` strategy, and non conforming floats with the `.throw` strategy.
To customize the JSON format, provide an explicit implementation for the `DatabaseValueConvertible` requirements, or implement these two methods:
```swift
protocol DatabaseValueConvertible {
static func databaseJSONDecoder() -> JSONDecoder
static func databaseJSONEncoder() -> JSONEncoder
}
```
### Adding support for the Tagged library
[Tagged](https://github.com/pointfreeco/swift-tagged) is a popular library that makes it possible to enhance the type-safety of our programs with dedicated wrappers around basic types. For example:
```swift
import Tagged
struct Player: Identifiable {
// Thanks to Tagged, Player.ID can not be mismatched with Team.ID or
// Award.ID, even though they all wrap strings.
typealias ID = Tagged<Player, String>
var id: ID
var name: String
var score: Int
}
```
Applications that use both Tagged and GRDB will want to add those lines somewhere:
```swift
import GRDB
import Tagged
// Add database support to Tagged values
extension Tagged: SQLExpressible where RawValue: SQLExpressible { }
extension Tagged: StatementBinding where RawValue: StatementBinding { }
extension Tagged: StatementColumnConvertible where RawValue: StatementColumnConvertible { }
extension Tagged: DatabaseValueConvertible where RawValue: DatabaseValueConvertible { }
```
This makes it possible to use `Tagged` values in all the expected places:
```swift
let id: Player.ID = ...
let player = try Player.find(db, id: id)
```
## Optimized Values
For extra performance, custom value types can conform to both `DatabaseValueConvertible` and ``StatementColumnConvertible``. This extra protocol grants raw access to the [low-level C SQLite interface](https://www.sqlite.org/c3ref/column_blob.html) when decoding values.
For example:
```swift
extension EvenInteger: StatementColumnConvertible {
init?(sqliteStatement: SQLiteStatement, index: CInt) {
let int64 = sqlite3_column_int64(sqliteStatement, index)
guard let value = Int(exactly: int64) else {
return nil // Does not fit Int (probably a 32-bit architecture)
}
self.init(value) // Nil if not even
}
}
```
This extra conformance is not required: only aim at the low-level C interface if you have identified a performance issue after profiling your application!
## Topics
### Creating a Value
- ``fromDatabaseValue(_:)-21zzv``
- ``fromMissingColumn()-7iamp``
### Accessing the DatabaseValue
- ``databaseValue-1ob9k``
### Configuring the JSON format for the standard Decodable protocol
- ``databaseJSONDecoder()-7zou9``
- ``databaseJSONEncoder()-37sff``
### Fetching Values from Raw SQL
- ``fetchCursor(_:sql:arguments:adapter:)-6elcz``
- ``fetchAll(_:sql:arguments:adapter:)-1cqyb``
- ``fetchSet(_:sql:arguments:adapter:)-5jene``
- ``fetchOne(_:sql:arguments:adapter:)-qvqp``
### Fetching Values from a Prepared Statement
- ``fetchCursor(_:arguments:adapter:)-4l6af``
- ``fetchAll(_:arguments:adapter:)-3abuc``
- ``fetchSet(_:arguments:adapter:)-6y54n``
- ``fetchOne(_:arguments:adapter:)-3d7ax``
### Fetching Values from a Request
- ``fetchCursor(_:_:)-8q4r6``
- ``fetchAll(_:_:)-9hkqs``
- ``fetchSet(_:_:)-1foke``
- ``fetchOne(_:_:)-o6yj``
### Supporting Types
- ``DatabaseValueCursor``
- ``StatementBinding``
@@ -0,0 +1,205 @@
# ``GRDB/Statement``
A prepared statement.
## Overview
Prepared statements let you execute an SQL query several times, with different arguments if needed.
Reusing prepared statements is a performance optimization technique because SQLite parses and analyses the SQL query only once, when the prepared statement is created.
## Building Prepared Statements
Build a prepared statement with the ``Database/makeStatement(sql:)`` method:
```swift
try dbQueue.write { db in
let insertStatement = try db.makeStatement(sql: """
INSERT INTO player (name, score) VALUES (:name, :score)
""")
let selectStatement = try db.makeStatement(sql: """
SELECT * FROM player WHERE name = ?
""")
}
```
The `?` and colon-prefixed keys like `:name` in the SQL query are the statement arguments. Set the values for those arguments with arrays or dictionaries of database values, or ``StatementArguments`` instances:
```swift
insertStatement.arguments = ["name": "Arthur", "score": 1000]
selectStatement.arguments = ["Arthur"]
```
Alternatively, the ``Database/makeStatement(literal:)`` method creates prepared statements with support for [SQL Interpolation]:
```swift
let insertStatement = try db.makeStatement(literal: "INSERT ...")
let selectStatement = try db.makeStatement(literal: "SELECT ...")
// ~~~~~~~
```
The `makeStatement` methods throw an error of code `SQLITE_MISUSE` (21) if the SQL query contains multiple statements joined with a semicolon. See <doc:GRDB/Statement#Parsing-Multiple-Prepared-Statements-from-a-Single-SQL-String> below.
## Executing Prepared Statements and Fetching Values
Prepared statements can be executed:
```swift
try insertStatement.execute()
```
To fetch rows and values from a prepared statement, use a fetching method of ``Row``, ``DatabaseValueConvertible``, or ``FetchableRecord``:
```swift
let players = try Player.fetchCursor(selectStatement) // A Cursor of Player
let players = try Player.fetchAll(selectStatement) // [Player]
let players = try Player.fetchSet(selectStatement) // Set<Player>
let player = try Player.fetchOne(selectStatement) // Player?
// ~~~~~~ or Row, Int, String, Date, etc.
```
Arguments can be set at the moment of the statement execution:
```swift
try insertStatement.execute(arguments: ["name": "Arthur", "score": 1000])
let player = try Player.fetchOne(selectStatement, arguments: ["Arthur"])
```
> Note: A prepared statement that has failed with an error can not be recovered. Create a new instance, or use a cached statement as described below.
> Tip: When you look after the best performance, take care about a difference between setting the arguments before execution, and setting the arguments at the moment of execution:
>
> ```swift
> // First option
> try statement.setArguments(...)
> try statement.execute()
>
> // Second option
> try statement.execute(arguments: ...)
> ```
>
> Both perform exactly the same action, and most applications should not care about the difference. Yet:
>
> - ``setArguments(_:)`` performs a copy of string and blob arguments. It uses the low-level [`SQLITE_TRANSIENT`](https://www.sqlite.org/c3ref/c_static.html) option, and fits well the reuse of a given statement with the same arguments.
> - ``execute(arguments:)`` avoids a temporary allocation for string and blob arguments if the number of arguments is small. Instead of `SQLITE_TRANSIENT`, it uses the low-level [`SQLITE_STATIC`](https://www.sqlite.org/c3ref/c_static.html) option. This fits well the reuse of a given statement with various arguments.
>
> Don't make a blind choice, and monitor your app performance if it really matters!
## Caching Prepared Statements
When the same query will be used several times in the lifetime of an application, one may feel a natural desire to cache prepared statements.
Don't cache statements yourself.
> Note: This is because an application lacks the necessary tools. Statements are tied to specific SQLite connections and dispatch queues which are not managed by the application, especially with a ``DatabasePool`` connection. A change in the database schema [may, or may not](https://www.sqlite.org/compile.html#max_schema_retry) invalidate a statement.
Instead, use the ``Database/cachedStatement(sql:)`` method. GRDB does all the hard caching and memory management:
```swift
let statement = try db.cachedStatement(sql: "INSERT ...")
```
The variant ``Database/cachedStatement(literal:)`` supports [SQL Interpolation]:
```swift
let statement = try db.cachedStatement(literal: "INSERT ...")
```
Should a cached prepared statement throw an error, don't reuse it. Instead, reload one from the cache.
## Parsing Multiple Prepared Statements from a Single SQL String
To build multiple statements joined with a semicolon, use ``Database/allStatements(sql:arguments:)``:
```swift
let statements = try db.allStatements(sql: """
INSERT INTO player (name, score) VALUES (?, ?);
INSERT INTO player (name, score) VALUES (?, ?);
""", arguments: ["Arthur", 100, "O'Brien", 1000])
while let statement = try statements.next() {
try statement.execute()
}
```
The variant ``Database/allStatements(literal:)`` supports [SQL Interpolation]:
```swift
let statements = try db.allStatements(literal: """
INSERT INTO player (name, score) VALUES (\("Arthur"), \(100));
INSERT INTO player (name, score) VALUES (\("O'Brien"), \(1000));
""")
// An alternative way to iterate all statements
try statements.forEach { statement in
try statement.execute()
}
```
> Tip: When you intend to run all statements in an SQL string but don't care about individual ones, don't bother iterating individual statement instances! Skip this documentation section and just use ``Database/execute(sql:arguments:)``:
>
> ```swift
> try db.execute(sql: """
> CREATE TABLE player ...;
> INSERT INTO player ...;
> """)
> ```
The results of multiple `SELECT` statements can be joined into a single ``Cursor``. This is the GRDB version of the [`sqlite3_exec()`](https://www.sqlite.org/c3ref/exec.html) function:
```swift
let statements = try db.allStatements(sql: """
SELECT ...;
SELECT ...;
""")
let players = try statements.flatMap { statement in
try Player.fetchCursor(statement)
}
for let player = try players.next() {
print(player.name)
}
```
The ``SQLStatementCursor`` returned from `allStatements` can be turned into a regular Swift array, but in this case make sure all individual statements can compile even if the previous ones were not executed:
```swift
// OK: Array of statements
let statements = try Array(db.allStatements(sql: """
INSERT ...;
UPDATE ...;
"""))
// FAILURE: Can't build an array of statements since the INSERT won't
// compile until CREATE TABLE is executed.
let statements = try Array(db.allStatements(sql: """
CREATE TABLE player ...;
INSERT INTO player ...;
"""))
```
## Topics
### Executing a Prepared Statement
- ``execute(arguments:)``
### Arguments
- ``arguments``
- ``setArguments(_:)``
- ``setUncheckedArguments(_:)``
- ``validateArguments(_:)``
- ``StatementArguments``
### Statement Informations
- ``columnCount``
- ``columnNames``
- ``databaseRegion``
- ``index(ofColumn:)``
- ``isReadonly``
- ``sql``
- ``sqliteStatement``
- ``SQLiteStatement``
[SQL Interpolation]: https://github.com/groue/GRDB.swift/blob/master/Documentation/SQLInterpolation.md
@@ -0,0 +1,285 @@
# ``GRDB/TransactionObserver``
A type that tracks database changes and transactions performed in a database.
## Overview
`TransactionObserver` is the low-level protocol that supports all <doc:DatabaseObservation> features.
A transaction observer is notified of individual changes (inserts, updates and deletes), before they are committed to disk, as well as transaction commits and rollbacks.
## Activate a Transaction Observer
An observer starts receiving change notifications after it has been added to a database connection with the ``DatabaseWriter/add(transactionObserver:extent:)`` `DatabaseWriter` method, or the ``Database/add(transactionObserver:extent:)`` `Database` method:
```swift
let observer = MyObserver()
dbQueue.add(transactionObserver: observer)
```
By default, database holds weak references to its transaction observers: they are not retained, and stop getting notifications after they are deallocated. See <doc:TransactionObserver#Observation-Extent> for more options.
## Database Changes And Transactions
Database changes are notified to the ``databaseDidChange(with:)`` callback. This includes indirect changes triggered by `ON DELETE` and `ON UPDATE` actions associated to [foreign keys](https://www.sqlite.org/foreignkeys.html#fk_actions), and [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
Transaction completions are notified to the ``databaseWillCommit()-7mksu``, ``databaseDidCommit(_:)`` and ``databaseDidRollback(_:)`` callbacks.
> Important: Some changes and transactions are not automatically notified. See <doc:GRDB/TransactionObserver#Dealing-with-Undetected-Changes> below.
Notified changes are not actually written to disk until the transaction commits, and the `databaseDidCommit` callback is called. On the other side, `databaseDidRollback` confirms their invalidation:
```swift
try dbQueue.write { db in
try db.execute(sql: "INSERT ...") // 1. didChange
try db.execute(sql: "UPDATE ...") // 2. didChange
} // 3. willCommit, 4. didCommit
try dbQueue.inTransaction { db in
try db.execute(sql: "INSERT ...") // 1. didChange
try db.execute(sql: "UPDATE ...") // 2. didChange
return .rollback // 3. didRollback
}
try dbQueue.write { db in
try db.execute(sql: "INSERT ...") // 1. didChange
throw SomeError()
} // 2. didRollback
```
Database statements that are executed outside of any explicit transaction do not drop off the radar:
```swift
try dbQueue.writeWithoutTransaction { db in
try db.execute(sql: "INSERT ...") // 1. didChange, 2. willCommit, 3. didCommit
try db.execute(sql: "UPDATE ...") // 4. didChange, 5. willCommit, 6. didCommit
}
```
Changes that are on hold because of a [savepoint](https://www.sqlite.org/lang_savepoint.html) are only notified after the savepoint has been released. This makes sure that notified events are only those that have an opportunity to be committed:
```swift
try dbQueue.inTransaction { db in
try db.execute(sql: "INSERT ...") // 1. didChange
try db.execute(sql: "SAVEPOINT foo")
try db.execute(sql: "UPDATE ...") // delayed
try db.execute(sql: "UPDATE ...") // delayed
try db.execute(sql: "RELEASE SAVEPOINT foo") // 2. didChange, 3. didChange
try db.execute(sql: "SAVEPOINT bar")
try db.execute(sql: "UPDATE ...") // not notified
try db.execute(sql: "ROLLBACK TO SAVEPOINT bar")
try db.execute(sql: "RELEASE SAVEPOINT bar")
return .commit // 4. willCommit, 5. didCommit
}
```
Eventual errors thrown from `databaseWillCommit` are exposed to the application code:
```swift
do {
try dbQueue.inTransaction { db in
...
return .commit // 1. willCommit (throws), 2. didRollback
}
} catch {
// 3. The error thrown by the transaction observer.
}
```
- Note: All callbacks are called in the writer dispatch queue, and serialized with all database updates.
- Note: The `databaseDidChange` and `databaseWillCommit` callbacks must not access the observed writer database connection in any way. This limitation does not apply to `databaseDidCommit` and `databaseDidRollback` which can use their database argument.
## Filtering Database Events
**Transaction observers can choose the database changes they are interested in.**
The ``observes(eventsOfKind:)`` method filters events that are notified to ``databaseDidChange(with:)``. It is the most efficient and recommended change filtering technique, because it is only called once before a database query is executed, and can completely disable change tracking:
```swift
// Calls `observes(eventsOfKind:)` once.
// Calls `databaseDidChange(with:)` for every updated row, or not at all.
try db.execute(sql: "UPDATE player SET score = score + 1")
```
The ``DatabaseEventKind`` argument of `observes(eventsOfKind:)` can distinguish insertions from deletions and updates, and is also able to tell the columns that are about to be changed.
For example, an observer can focus on the changes that happen on the "player" database table only:
```swift
class PlayerObserver: TransactionObserver {
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
// Only observe changes to the "player" table.
eventKind.tableName == "player"
}
func databaseDidChange(with event: DatabaseEvent) {
// This method is only called for changes that happen to
// the "player" table.
}
}
```
When the `observes(eventsOfKind:)` method returns false for all event kinds, the observer is still notified of transactions.
## Observation Extent
**You can specify how long an observer is notified of database changes and transactions.**
The `remove(transactionObserver:)` method explicitly stops notifications, at any time:
```swift
// From a database queue or pool:
dbQueue.remove(transactionObserver: observer)
// From a database connection:
dbQueue.inDatabase { db in
db.remove(transactionObserver: observer)
}
```
Alternatively, use the `extent` parameter of the `add(transactionObserver:extent:)` method:
```swift
let observer = MyObserver()
// On a database queue or pool:
dbQueue.add(transactionObserver: observer) // default extent
dbQueue.add(transactionObserver: observer, extent: .observerLifetime)
dbQueue.add(transactionObserver: observer, extent: .nextTransaction)
dbQueue.add(transactionObserver: observer, extent: .databaseLifetime)
// On a database connection:
dbQueue.inDatabase { db in
db.add(transactionObserver: ...)
}
```
- The default extent is `.observerLifetime`: the database holds a weak reference to the observer, and the observation automatically ends when the observer is deallocated. Meanwhile, the observer is notified of all changes and transactions.
- `.nextTransaction` activates the observer until the current or next transaction completes. The database keeps a strong reference to the observer until its `databaseDidCommit` or `databaseDidRollback` callback is called. Hereafter the observer won't get any further notification.
- `.databaseLifetime` has the database retain and notify the observer until the database connection is closed.
Finally, an observer can avoid processing database changes until the end of the current transaction. After ``stopObservingDatabaseChangesUntilNextTransaction()``, the `databaseDidChange` callback will not be called until the current transaction completes:
```swift
class PlayerObserver: TransactionObserver {
var playerTableWasModified = false
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
eventKind.tableName == "player"
}
func databaseDidChange(with event: DatabaseEvent) {
playerTableWasModified = true
// It is pointless to keep on tracking further changes:
stopObservingDatabaseChangesUntilNextTransaction()
}
}
```
## Support for SQLite Pre-Update Hooks
When SQLite is built with the `SQLITE_ENABLE_PREUPDATE_HOOK` option, `TransactionObserver` gets an extra callback which lets you observe individual column values in the rows modified by a transaction:
```swift
protocol TransactionObserver: AnyObject {
#if SQLITE_ENABLE_PREUPDATE_HOOK
/// Notifies before a database change (insert, update, or delete)
/// with change information (initial / final values for the row's
/// columns).
///
/// The event is only valid for the duration of this method call. If you
/// need to keep it longer, store a copy: event.copy().
func databaseWillChange(with event: DatabasePreUpdateEvent)
#endif
}
```
This extra API can be activated in two ways:
1. Use the GRDB.swift CocoaPod with a custom compilation option, as below.
It uses the system SQLite, which is compiled with `SQLITE_ENABLE_PREUPDATE_HOOK` support, but only on iOS 11.0+ (we don't know the minimum version of macOS, tvOS, watchOS):
```ruby
pod 'GRDB.swift'
platform :ios, '11.0' # or above
post_install do |installer|
installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target|
target.build_configurations.each do |config|
# Enable extra GRDB APIs
config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK"
# Enable extra SQLite APIs
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1"
end
end
end
```
**Warning**: make sure you use the right platform version! You will get runtime errors on devices with a lower version.
**Note**: the `GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1` option in `GCC_PREPROCESSOR_DEFINITIONS` defines some C function prototypes that are lacking from the system `<sqlite3.h>` header. When Xcode eventually ships with an SDK that includes a complete header, you may get a compiler error about duplicate function definitions. When this happens, just remove this `GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1` option.
2. Use a [custom SQLite build](http://github.com/groue/GRDB.swift/blob/master/Documentation/CustomSQLiteBuilds.md) and activate the `SQLITE_ENABLE_PREUPDATE_HOOK` compilation option.
## Dealing with Undetected Changes
The changes and transactions that are not automatically notified to transaction observers are:
- Read-only transactions.
- Changes and transactions performed by external database connections.
- Changes performed by SQLite statements that are not both compiled and executed through GRDB APIs.
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
- The deletion of duplicate rows triggered by [`ON CONFLICT REPLACE`](https://www.sqlite.org/lang_conflict.html) clauses (this last exception might change in a future release of SQLite).
To notify undetected changes to transaction observers, perform an explicit call to the ``Database/notifyChanges(in:)`` `Database` method. The ``databaseDidChange()-7olv7`` callback will be called accordingly. For example:
```swift
try dbQueue.write { db in
// Notify observers that some changes were performed in the database
try db.notifyChanges(in: .fullDatabase)
// Notify observers that some changes were performed in the player table
try db.notifyChanges(in: Player.all())
// Equivalent alternative
try db.notifyChanges(in: Table("player"))
}
```
To notify a change in the database schema, notify a change to the `sqlite_master` table:
```swift
try dbQueue.write { db in
// Notify all observers of the sqlite_master table
try db.notifyChanges(in: Table("sqlite_master"))
}
```
## Topics
### Filtering Database Changes
- ``observes(eventsOfKind:)``
- ``DatabaseEventKind``
### Handling Database Changes
- ``databaseDidChange()-7olv7``
- ``databaseDidChange(with:)``
- ``stopObservingDatabaseChangesUntilNextTransaction()``
- ``DatabaseEvent``
### Handling Transactions
- ``databaseWillCommit()-7mksu``
- ``databaseDidCommit(_:)``
- ``databaseDidRollback(_:)``
@@ -0,0 +1,318 @@
# ``GRDB/ValueObservation``
`ValueObservation` tracks changes in the results of database requests, and notifies fresh values whenever the database changes.
## Overview
`ValueObservation` tracks insertions, updates, and deletions that impact the tracked value, whether performed with raw SQL, or <doc:QueryInterface>. This includes indirect changes triggered by [foreign keys actions](https://www.sqlite.org/foreignkeys.html#fk_actions) or [SQL triggers](https://www.sqlite.org/lang_createtrigger.html).
See <doc:GRDB/ValueObservation#Dealing-with-Undetected-Changes> below for the list of exceptions.
## ValueObservation Usage
1. Make sure that a unique database connection, ``DatabaseQueue`` or ``DatabasePool``, is kept open during the whole duration of the observation.
2. Create a `ValueObservation` with a closure that fetches the observed value:
```swift
let observation = ValueObservation.tracking { db in
// Fetch and return the observed value
}
// For example, an observation of [Player], which tracks all players:
let observation = ValueObservation.tracking { db in
try Player.fetchAll(db)
}
// The same observation, using shorthand notation:
let observation = ValueObservation.tracking(Player.fetchAll)
```
There is no limit on the values that can be observed. An observation can perform multiple requests, from multiple database tables, and use raw SQL. See ``tracking(_:)`` for some examples.
3. Start the observation in order to be notified of changes:
```swift
let cancellable = observation.start(in: dbQueue) { error in
// Handle error
} onChange: { (players: [Player]) in
print("Fresh players", players)
}
```
4. Stop the observation by calling the ``DatabaseCancellable/cancel()`` method on the object returned by the `start` method. Cancellation is automatic when the cancellable is deallocated:
```swift
cancellable.cancel()
```
`ValueObservation` can also be turned into an async sequence, a Combine publisher, or an RxSwift observable (see the companion library [RxGRDB](https://github.com/RxSwiftCommunity/RxGRDB)):
- Async sequence:
```swift
do {
for try await players in observation.values(in: dbQueue) {
print("Fresh players", players)
}
} catch {
// Handle error
}
```
- Combine Publisher:
```swift
let cancellable = observation.publisher(in: dbQueue).sink { completion in
// Handle completion
} receiveValue: { (players: [Player]) in
print("Fresh players", players)
}
```
## ValueObservation Behavior
`ValueObservation` notifies an initial value before the eventual changes.
`ValueObservation` only notifies changes committed to disk.
By default, `ValueObservation` notifies a fresh value whenever any component of its fetched value is modified (any fetched column, row, etc.). This can be configured: see <doc:ValueObservation#Specifying-the-Tracked-Region>.
By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously. This can be configured: see <doc:ValueObservation#ValueObservation-Scheduling>.
By default, `ValueObservation` fetches a fresh value immediately after a change is committed in the database. In particular, modifying the database on the main thread triggers a fetch on the main thread as well. This behavior can be configured: see <doc:ValueObservation#ValueObservation-Scheduling>.
`ValueObservation` may coalesce subsequent changes into a single notification.
`ValueObservation` may notify consecutive identical values. You can filter out the undesired duplicates with the ``removeDuplicates()`` method.
Starting an observation retains the database connection, until it is stopped. As long as the observation is active, the database connection won't be deallocated.
The database observation stops when the cancellable returned by the `start` method is cancelled or deallocated, or if an error occurs.
> Important: Take care that there are use cases that `ValueObservation` is unfit for.
>
> For example, an application may need to process absolutely all changes, and avoid any coalescing. An application may also need to process changes before any further modifications could be performed in the database file. In those cases, the application needs to track *individual transactions*, not values: use ``DatabaseRegionObservation``.
>
> If you need to process changes before they are committed to disk, use ``TransactionObserver``.
## ValueObservation Scheduling
By default, `ValueObservation` notifies the initial value, as well as eventual changes and errors, on the main dispatch queue, asynchronously:
```swift
// The default scheduling
let cancellable = observation.start(in: dbQueue) { error in
// Called asynchronously on the main dispatch queue
} onChange: { value in
// Called asynchronously on the main dispatch queue
print("Fresh value", value)
}
```
You can change this behavior by adding a `scheduling` argument to the `start()` method.
For example, the ``ValueObservationScheduler/immediate`` scheduler notifies all values on the main dispatch queue, and notifies the first one immediately when the observation starts.
It is very useful in graphic applications, because you can configure views right away, without waiting for the initial value to be fetched eventually. You don't have to implement any empty or loading screen, or to prevent some undesired initial animation. Take care that the user interface is not responsive during the fetch of the first value, so only use the `immediate` scheduling for very fast database requests!
The `immediate` scheduling requires that the observation starts from the main dispatch queue (a fatal error is raised otherwise):
```swift
// Immediate scheduling notifies
// the initial value right on subscription.
let cancellable = observation
.start(in: dbQueue, scheduling: .immediate) { error in
// Called on the main dispatch queue
} onChange: { value in
// Called on the main dispatch queue
print("Fresh value", value)
}
// <- Here "Fresh value" has already been printed.
```
The other built-in scheduler ``ValueObservationScheduler/async(onQueue:)`` asynchronously schedules values and errors on the dispatch queue of your choice. Make sure you provide a serial queue, because a concurrent one such as `DispachQueue.global(qos: .default)` would mess with the ordering of fresh value notifications:
```swift
// Async scheduling notifies all values
// on the specified dispatch queue.
let myQueue: DispatchQueue
let cancellable = observation
.start(in: dbQueue, scheduling: .async(myQueue)) { error in
// Called asynchronously on myQueue
} onChange: { value in
// Called asynchronously on myQueue
print("Fresh value", value)
}
```
As described above, the `scheduling` argument controls the execution of the change and error callbacks. You also have some control on the execution of the database fetch:
- With the `.immediate` scheduling, the initial fetch is always performed synchronously, on the main thread, when the observation starts, so that the initial value can be notified immediately.
- With the default `.async` scheduling, the initial fetch is always performed asynchronouly. It never blocks the main thread.
- By default, fresh values are fetched immediately after the database was changed. In particular, modifying the database on the main thread triggers a fetch on the main thread as well.
To change this behavior, and guarantee that fresh values are never fetched from the main thread, you need a ``DatabasePool`` and an optimized observation created with the ``tracking(regions:fetch:)`` or ``trackingConstantRegion(_:)`` methods. Make sure you read the documentation of those methods, or you might write an observation that misses some database changes.
It is possible to use a ``DatabasePool`` in the application, and an in-memory ``DatabaseQueue`` in tests and Xcode previews, with the common protocol ``DatabaseWriter``.
## ValueObservation Sharing
Sharing a `ValueObservation` spares database resources. When a database change happens, a fresh value is fetched only once, and then notified to all clients of the shared observation.
You build a shared observation with ``shared(in:scheduling:extent:)``:
```swift
// SharedValueObservation<[Player]>
let sharedObservation = ValueObservation
.tracking { db in try Player.fetchAll(db) }
.shared(in: dbQueue)
```
`ValueObservation` and `SharedValueObservation` are nearly identical, but the latter has no operator such as `map`. As a replacement, you may for example use Combine apis:
```swift
let cancellable = try sharedObservation
.publisher() // Turn shared observation into a Combine Publisher
.map { ... } // The map operator from Combine
.sink(...)
```
## Specifying the Tracked Region
While the standard ``tracking(_:)`` method lets you track changes to a fetched value and receive any changes to it, sometimes your use case might require more granular control.
Consider a scenario where you'd like to get a specific Player's row, but only when their `score` column changes. You can use ``tracking(region:_:fetch:)`` to do just that:
```swift
let observation = ValueObservation.tracking(
// Define the tracked database region
// (the score column of the player with id 1)
region: Player.select(Column("score")).filter(id: 1),
// Define what to fetch upon such change to the tracked region
// (the player with id 1)
fetch: { db in try Player.fetchOne(db, id: 1) }
)
```
This ``tracking(region:_:fetch:)`` method lets you entirely separate the **observed region(s)** from the **fetched value** itself, for maximum flexibility. See ``DatabaseRegionConvertible`` for more information about the regions that can be tracked.
## Dealing with Undetected Changes
`ValueObservation` will not fetch and notify a fresh value whenever the database is modified in an undetectable way:
- Changes performed by external database connections.
- Changes performed by SQLite statements that are not compiled and executed by GRDB.
- Changes to the database schema, changes to internal system tables such as `sqlite_master`.
- Changes to [`WITHOUT ROWID`](https://www.sqlite.org/withoutrowid.html) tables.
To have observations notify a fresh values after such an undetected change was performed, applications can take explicit action. For example, cancel and restart observations. Alternatively, call the ``Database/notifyChanges(in:)`` `Database` method from a write transaction:
```swift
try dbQueue.write { db in
// Notify observations that some changes were performed in the database
try db.notifyChanges(in: .fullDatabase)
// Notify observations that some changes were performed in the player table
try db.notifyChanges(in: Player.all())
// Equivalent alternative
try db.notifyChanges(in: Table("player"))
}
```
## ValueObservation Performance
This section further describes runtime aspects of `ValueObservation`, and provides some optimization tips for demanding applications.
**`ValueObservation` is triggered by database transactions that may modify the tracked value.**
Precisely speaking, `ValueObservation` tracks changes in a ``DatabaseRegion``, not changes in values.
For example, if you track the maximum score of players, all transactions that impact the `score` column of the `player` database table (any update, insertion, or deletion) trigger the observation, even if the maximum score itself is not changed.
You can filter out undesired duplicate notifications with the ``removeDuplicates()`` method.
**ValueObservation can create database contention.** In other words, active observations take a toll on the constrained database resources. When triggered by impactful transactions, observations fetch fresh values, and can delay read and write database accesses of other application components.
When needed, you can help GRDB optimize observations and reduce database contention:
> Tip: Stop observations when possible.
>
> For example, if a `UIViewController` needs to display database values, it can start the observation in `viewWillAppear`, and stop it in `viewWillDisappear`.
>
> In a SwiftUI application, you can profit from the [GRDBQuery](https://github.com/groue/GRDBQuery) companion library, and its [`View.queryObservation(_:)`](https://swiftpackageindex.com/groue/grdbquery/documentation/grdbquery/queryobservation) method.
> Tip: Share observations when possible.
>
> Each call to `ValueObservation.start` method triggers independent values refreshes. When several components of your app are interested in the same value, consider sharing the observation with ``shared(in:scheduling:extent:)``.
> Tip: When the observation processes some raw fetched values, use the ``map(_:)`` operator:
>
> ```swift
> // Plain observation
> let observation = ValueObservation.tracking { db -> MyValue in
> let players = try Player.fetchAll(db)
> return computeMyValue(players)
> }
>
> // Optimized observation
> let observation = ValueObservation
> .tracking { db try Player.fetchAll(db) }
> .map { players in computeMyValue(players) }
> ```
>
> The `map` operator performs its job without blocking database accesses, and without blocking the main thread.
> Tip: When the observation tracks a constant database region, create an optimized observation with the ``tracking(regions:fetch:)`` or ``trackingConstantRegion(_:)`` methods. Make sure you read the documentation of those methods, or you might write an observation that misses some database changes.
**Truncating WAL checkpoints impact ValueObservation.** Such checkpoints are performed with ``Database/checkpoint(_:on:)`` or [`PRAGMA wal_checkpoint`](https://www.sqlite.org/pragma.html#pragma_wal_checkpoint). When an observation is started on a ``DatabasePool``, from a database that has a missing or empty [wal file](https://www.sqlite.org/tempfiles.html#write_ahead_log_wal_files), the observation will always notify two values when it starts, even if the database content is not changed. This is a consequence of the impossibility to create the [wal snapshot](https://www.sqlite.org/c3ref/snapshot_get.html) needed for detecting that no changes were performed during the observation startup. If your application performs truncating checkpoints, you will avoid this behavior if you recreate a non-empty wal file before starting observations. To do so, perform any kind of no-op transaction (such a creating and dropping a dummy table).
## Topics
### Creating a ValueObservation
- ``tracking(_:)``
- ``trackingConstantRegion(_:)``
- ``tracking(region:_:fetch:)``
- ``tracking(regions:fetch:)``
### Creating a Shared Observation
- ``shared(in:scheduling:extent:)``
- ``SharedValueObservationExtent``
### Accessing Observed Values
- ``publisher(in:scheduling:)``
- ``start(in:scheduling:onError:onChange:)``
- ``values(in:scheduling:bufferingPolicy:)``
- ``DatabaseCancellable``
- ``ValueObservationScheduler``
### Mapping Values
- ``map(_:)``
### Filtering Values
- ``removeDuplicates()``
- ``removeDuplicates(by:)``
### Requiring Write Access
- ``requiresWriteAccess``
### Debugging
- ``handleEvents(willStart:willFetch:willTrackRegion:databaseDidChange:didReceiveValue:didFail:didCancel:)``
- ``print(_:to:)``
### Support
- ``ValueReducer``