|

GRDB: Turning SQLite Queries into Live Application State

Daily (almost) Swift Open Source Overview hero for GRDB

SQLite gives an app a durable source of truth, but a UI needs something more immediate: when the data behind a screen changes, the screen needs a fresh value. A common solution is to add another layer above the database. A repository posts notifications, a view model subscribes to them, and every write has to remember which signals to send. That works, but it creates a second change-tracking system beside SQLite. GRDB takes a different route. Its ValueObservation API connects a database read to the part of the database that can affect that read. When a relevant transaction commits, GRDB fetches again and delivers a new value. That makes database observation more interesting than “run this query whenever anything changes.”

From a query to a stream of values

Consider a scoreboard stored in SQLite. The model can stay small:

import GRDB

struct Player: Codable, FetchableRecord, Sendable {
    var id: Int64
    var name: String
    var score: Int
}

Now the read can become an observation:

let observation = ValueObservation.tracking { db in
    try Player.fetchAll(
        db,
        sql: """
        SELECT id, name, score
        FROM player
        ORDER BY score DESC
        """)
}

for try await players in observation.values(in: dbPool) {
    render(players)
}

render is application code here; the GRDB part ends at the async sequence. The important detail is not the loop. It is what happens when the observation starts. GRDB executes the fetch and records the database region accessed by it. The observation can then react to changes that affect that region instead of treating every write as equally relevant. The current ValueObservation API can also deliver values through callbacks, and it exposes a Combine publisher when Combine is available. The async version uses AsyncThrowingStream internally, so observation cancellation follows the lifetime of the async iterator.

A database region is more precise than a global notification

The type behind this idea is DatabaseRegion. A region can describe the whole database, but it can also describe parts of it: tables, sets of columns, and, where GRDB has enough information, row IDs. Regions can be combined, and GRDB can ask whether a SQLite database event can modify a region. This gives observation a useful property: the read itself helps define what should wake it up. GRDB’s tests show this directly. An observation can explicitly track one table while writes to another table are ignored. The library also tests narrower regions such as selected columns and individual row IDs. You can provide the region yourself when it is already known:

let observation = ValueObservation.tracking(
    region: Table("player"),
    fetch: { db in
        try Int.fetchOne(
            db,
            sql: "SELECT COUNT(*) FROM player WHERE score >= 100")!
    })

for try await count in observation.values(in: dbPool) {
    print("High scores:", count)
}

This version deliberately says that the full player table is relevant. It is simple and predictable. For many reads, though, letting GRDB record the accessed region avoids maintaining that information twice. There are also constant-region variants. They are useful when repeated fetches always touch the same database region. Regular tracking is the safer choice when the set of rows or tables involved can change between fetches.

Observation sits on top of SQLite transactions

GRDB does not maintain a parallel event store to make this work. At the lower level, TransactionObserver receives SQLite transaction events. DatabaseRegion can filter those events by table, columns, and row IDs. ValueObservation uses that machinery to decide when its reducer needs another fetched value. That distinction matters. A write is not the same thing as an observed value changing. First, GRDB decides whether the committed database change can affect the observed region. Then the observation fetches again and produces the next value. The source also contains an important fallback for virtual tables. Some SQLite virtual tables can perform changes in shadow tables that were not advertised as the original event. When GRDB receives such an unexpected event, the region code chooses the conservative answer and treats it as relevant rather than silently missing a possible change. That is a good example of the library preferring correctness over an optimization when SQLite cannot provide enough information.

DatabaseQueue and DatabasePool change the execution model

GRDB exposes two main database connection types. DatabaseQueue serializes access through one connection. Its initializer explicitly forces the maximum reader count to one. DatabasePool is designed for concurrent access. For observations that do not require write access, it can use a concurrent observation path. Its read implementation also contains careful SQLite WAL snapshot handling: before releasing the writer queue, it establishes the read snapshot so that the reader sees a stable committed state. This is more than an implementation detail. Observation has to answer two questions at once:

  1. Did a relevant change happen?
  2. Which committed database state should the next fetch see? If those answers can race, a live-query abstraction becomes unreliable. Much of the interesting code in GRDB is the coordination needed to preserve those guarantees while still allowing concurrency.

Swift concurrency is part of the current design

The current package manifest uses Swift tools 6.1 and Swift 6 language mode. GRDB 7 was the release line that introduced full Swift 6 support, including more Sendable APIs and task-aware asynchronous database access. Observation has evolved with that work. ValueObservation itself is Sendable, and its async sequence is built to work naturally with async code. The 7.x release history also contains fixes where task cancellation interacted with database access and observation. In 7.4.0, for example, the project fixed unexpected ValueObservation failures caused by transaction observers being affected by task cancellation. This is a useful reminder that wrapping a callback API in AsyncSequence is the easy part. Cancellation, transaction state, and the lifetime of the underlying observer still have to agree.

Where this approach fits

GRDB’s observation model is especially useful when SQLite is already the source of truth for application state. Instead of writing data into SQLite and then mirroring the same change through a separate notification layer, the application can observe the query that produces the value it needs. It is less useful when the database is not the authority for that state, or when an application already has another state system that intentionally owns synchronization. Observation also does not make an expensive query cheap. A relevant change still causes a refetch, so query design, indexes, transaction shape, and the size of the observed region still matter. There is another practical limit: database observation depends on GRDB seeing the changes it needs to observe. The project documents strategies for changes that GRDB cannot detect automatically, rather than pretending every possible SQLite mutation is visible.

Source worth reading

If you want to understand the design, ValueObservation.swift is only the start. DatabaseRegion.swift shows how the library represents and combines observable parts of a database. TransactionObserver.swift connects that model to SQLite events. DatabasePool.swift is worth reading for the concurrency side, especially the code that establishes snapshot isolation before concurrent reads continue. The tests are equally useful. ValueObservationTests.swift contains cases for explicit regions, inferred regions, views, row IDs, scheduling, and concurrent observation behavior. They make the intended semantics much clearer than a list of API methods.

Project health

GRDB is actively maintained. The current changelog lists 7.11.1, released on June 18, 2026, as the latest release. The current package requires Swift 6.1+, with explicit deployment minimums of iOS 13, macOS 10.15, tvOS 13, and watchOS 7. The repository uses the MIT license.

Engineering takeaway

The most useful idea in GRDB’s observation system is not that SQLite can drive a UI. It is that the dependency between a value and the database can be described close to the read that creates that value. A query produces data. GRDB records what database region matters to that query. SQLite transaction events are filtered through that region. Only then does the value get fetched again. That removes a surprising amount of manual coordination from application code, while keeping SQLite as the place where the state actually lives.

Sources