The db property on ZonaiClient wraps the server's database endpoints. All
methods accept a body object that mirrors the corresponding REST request body.
Live UI: see Real-Time Streaming below
(client.db.listen). Server guide: Streaming (Live Queries).
Real-Time Streaming#
Zonai pushes live query updates over long-lived HTTP. Server routes:
GET /db/stream, /db/stream/list, /db/stream/count.
Full protocol notes: Streaming (Live Queries).
client.db.listen exposes three methods that keep a Stream open and emit whenever
the underlying SQLite result changes.
Stream a single record#
client.db.listen
.one(
body: StreamBody(
table: 'posts',
where: Eq('id', 'abc_ps'),
),
fromJson: (row) => row,
)
.listen((record) {
// Called on connect and whenever the record changes
});
Stream a list#
client.db.listen
.list(
body: StreamListBody(table: 'posts', limit: 20),
fromJson: (row) => row,
)
.listen((records) {
// Called whenever the result set changes
});
Stream a count#
client.db.listen
.count(body: StreamCountBody(table: 'posts', where: Eq('published', true)))
.listen((total) {
// Called whenever the count changes
});
Cancel the subscription when you no longer need updates:
final sub = client.db.listen
.list(
body: StreamListBody(table: 'posts'),
fromJson: (row) => row,
)
.listen((_) {});
// Later:
await sub.cancel();
CRUD Operations#
Get a single record#
final record = await client.db.get(
body: GetBody(table: 'posts', where: Eq('id', 'abc_ps')),
fromJson: (row) => row,
);
// record is Map<String, Object?>
List records#
final page = await client.db.list(
body: ListBody(table: 'posts', limit: 20),
fromJson: (row) => row,
);
// page is Paginated<Map<String, Object?>>
Count records#
final total = await client.db.count(
body: CountBody(table: 'posts'),
);
Create a record#
final created = await client.db.create(
body: CreateBody(table: 'posts', object: {'title': 'Hello'}),
fromJson: (row) => row,
);
Update records#
// Update every record matching a filter
await client.db.updateMany(
body: UpdateBody(
table: 'posts',
updates: [Update.column('published', UpdateValue.literal(true))],
where: Eq('author_id', 'abc_au'),
),
fromJson: (row) => row,
);
// Update exactly one record
await client.db.update(
body: UpdateOneBody(
table: 'posts',
updates: [Update.column('title', UpdateValue.literal('New title'))],
where: Eq('id', 'abc_ps'),
),
fromJson: (row) => row,
);
Update only if the row is still what you read#
expect is a precondition every target row must meet. If any row fails it, nothing is written and the update throws
PreconditionFailedException, whose current carries the failing rows as they are now. That's optimistic concurrency: read a row, then write it back only if nobody else changed it in between.
try {
await client.db.update(
body: UpdateOneBody(
table: 'posts',
where: Eq('id', 'abc_ps'),
updates: [Update.column('title', UpdateValue.literal('New title'))],
expect: Eq('rev', 3), // the revision you read
),
fromJson: (row) => row,
);
} on PreconditionFailedException catch (e) {
final now = e.current.single; // someone else's edit; reconcile with it
print('changed under us: $now');
}
-
A precondition failure is a
412, witherror.codeset toprecondition_failedand the rows inerror.details.current. It is not a404: a row that no longer matcheswhereat all still returns404fromupdate, or an empty list fromupdateMany. The two mean different things to a client that is reconciling. -
updateManyis refused whole when any target fails, andcurrentlists only the failing rows. currentholds only rows you may view.-
On the wire,
expectmust be a where-object. Missing ornullmeans no precondition; anything else ([],"rev=3") is a400with codeinvalid_expect, never an unconditional write. An object that isn't a valid where ({}, or an unknowntype) is a400with codeinvalid_body, the same answer as any other malformed request body. -
Every permission refusal comes first: a
403always wins over a412, and abeforeUpdatehook never runs for an update the precondition refuses. -
A server older than this feature ignores
expectand applies the update unconditionally. Know your server version before you rely on it.
Delete records#
// Delete every record matching a filter
await client.db.deleteMany(
body: DeleteBody(table: 'posts', where: Eq('author_id', 'abc_au')),
);
// Delete exactly one record
await client.db.delete(
body: DeleteOneBody(table: 'posts', where: Eq('id', 'abc_ps')),
);
Write across tables at once#
client.db.batch sends several creates, updates and deletes, across any tables, in one request, and either every write lands or none do. See
Batch Writes.
await client.db.batch(
body: BatchBody(
operations: [
BatchOperation.update(
UpdateOneBody(
table: 'posts',
where: Eq('id', 'abc_ps'),
updates: [Update.column('author_id', UpdateValue.literal('xyz_au'))],
),
),
BatchOperation.deleteMany(
DeleteBody(table: 'drafts', where: Eq('post_id', 'abc_ps')),
),
],
),
);