Reactive Queries
Use watch() when a screen, service, or cache needs a query result that stays
up to date. A watcher emits one complete snapshot when it is listened to, then
emits another snapshot after a committed change can affect the query.
final db = await SqliteDatabase.connect();
await db.executeRaw('''
CREATE TABLE todos (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
)
''');
final subscription = db
.table('todos')
.whereEquals('completed', 0)
.orderBy('id')
.watch()
.listen((todos) {
// Replace the current screen snapshot.
print(todos);
});
await db.executeRaw(
'INSERT INTO todos (id, title, completed) VALUES (?, ?, ?)',
[1, 'Ship the feature', 0],
);
await subscription.cancel();
await db.close();
Snapshot versus finite streams
watch() and watchRows() are live snapshot streams. Each event contains the
full current result, so a consumer can replace its state without calculating a
diff. watch() returns hydrated models; watchRows() returns QueryRow<T>
values when row metadata or relation state is needed.
The existing stream() and streamModels() APIs remain finite row streams for
processing a query result. They do not subscribe to later database changes.
What causes a refresh?
Ormed records the tables read by the query, including structured joins, relations, aggregates, unions, and nested subqueries. A watcher refreshes only when a committed change overlaps those dependencies. Opaque raw or custom SQL is treated conservatively and can refresh for any change.
Generated models are optional:
final pending = db.table('todos').whereEquals('completed', 0).watch();
The same API works with a registered model:
final pending = dataSource
.query<Todo>()
.whereEquals('completed', false)
.orderBy('id')
.watch();
Transactions
Changes made inside transaction() or a manually controlled transaction are
published after commit. A rollback does not produce a new snapshot.
await db.beginTransaction();
try {
await db.table('todos').create({
'id': 2,
'title': 'Review the release',
'completed': 0,
});
await db.commit();
} catch (_) {
await db.rollback();
rethrow;
}
Drivers that can observe writes made outside the Ormed query context can expose
the optional DriverChangeFeed contract. SQLite adapters do this for direct
adapter writes. The Drift integration uses the same
approach: construct one DriftDriverAdapter, pass it to Ormed, and pass
driver.driftExecutor to Drift's DatabaseConnection.
Adapters around synchronized backends can provide a synchronization callback:
final driver = DriftDriverAdapter(
libsql,
closeDelegate: true,
synchronize: libsql.sync,
);
final db = await OrmDatabase.connect(driver: driver);
await driver.sync(); // Pulls remote changes and refreshes Ormed watchers.
The adapter opens the wrapped Drift executor during OrmDatabase.connect(),
so applications do not need a separate ensureOpen() call. The callback is
conservatively treated as affecting every table because a synchronization
backend may not expose its changed-table set.
Always use the same adapter/executor pair. Writes made through an unrelated database connection cannot be observed by that adapter's watcher.
Lifecycle
Cancel subscriptions when the owning screen or service is disposed. Closing an
OrmDatabase closes its change feed and completes active watchers.