Skip to content

feat(truapi): observe core store queries as streams - #1125

Merged
valentunn merged 7 commits into
mainfrom
feat/store-observation
Oct 2, 2026
Merged

valentunn merged 7 commits into
mainfrom
feat/store-observation

Conversation

@valentunn

@valentunn valentunn commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part 2 of #964

Summary

Adds ability to observe database. Consumers can specify a sql query which results they want to observe. They get a stream where each emission = real change of the results of that query.

Implementation idea:

  1. Once per query sql infer the queried table names via preparing a query with a custom authorizer that records table read authorizations. We filter out sqlite-internal tables
  2. Install update hook on a writer connection that reports the changed tables
  3. For each update we trigger each observer that is intersted on modified tables
  4. The watcher re-runs the query sql. Since re-query might result in the same exact result (e.g. sql uses WHERE) we also de-dup by rows content

Notable design decision

  • Update hook does not normally fire for DELETE * FROM queries because sqlite might utilize trimming optimization. We fix that by disallowing such, with a side effect that each row will now be deleted on its own. An alternative solution would be to use per-table invalidation triggers, but this is more complex and schema-invasive. We can always switch to it later if we find a need to. For now I dont anticipate any that large delete queries where the difference would matter

valentunn and others added 3 commits October 1, 2026 15:20
`Db::observe(sql, |q| …)` streams a query's result: once when first
polled, then again after every commit that changes a table the query
reads. This is the store's counterpart of Room's Flow queries, which the
durable engine and its domains use to follow their own tables.

The writer's `update_hook` collects touched tables and `Db::write`
publishes them after `commit()` returns, so a woken observer always reads
the committed rows and a rolled-back write wakes nobody. Wakes go through
a `channel(0)` per observer, so a burst of commits costs one re-query.

The tables a query reads are found with SQLite's authorizer, once per SQL
constant, and matched against the schema's tables, which covers joins,
subqueries, views and `count(*)`. The query closure gets an
`ObservedStatement` rather than the connection, so it cannot run SQL the
detection did not see. The statement records the raw values of every row
it reads, and a re-query whose rows equal the last emission's emits
nothing, without requiring `PartialEq` on the result.

Every connection carries an authorizer that answers `Ignore` for deletes,
which makes SQLite delete row by row instead of truncating, so an
unconditional `DELETE FROM t` still reaches `update_hook`. A schema test
rejects `WITHOUT ROWID` core tables, which the hook never reports.

A failed re-query yields `Err` and keeps observing; the stream ends when
the database closes or the query's tables cannot be resolved.

Refs #964

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review feedback: a function should not embed several self-contained
pieces of logic. Table resolution becomes `names_read_by` (what the
authorizer reports) and `schema_tables` (the real tables), intersected by
`resolve_tables`. The same split applies to waking one observer
(`Observer::notify`), registering one (`Invalidation::register`), the
per-SQL table cache (`Db::tables`), the dedupe re-query loop
(`Db::next_emission`), installing the writer's hooks (`track_changes`)
and running a write transaction (`in_transaction`). Behaviour is
unchanged.

Refs #964

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unchanged re-query no longer loops inside the stream step waiting for
the next wake. `Db::requery` runs the query once and returns `None` when
the rows equal the last emission's, and the stream drops those with
`filter_map`. Each wake now runs exactly one query, and waiting for a
wake happens in one place.

Refs #964

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@valentunn
valentunn requested a review from a team October 1, 2026 13:21
@valentunn valentunn added the no-changeset No published artifact changes; changeset not required label Oct 1, 2026
@github-actions github-actions Bot added the rust Pull requests that update rust code label Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Bundle size report

Compared with main at a3da648.

Raw Gzip Brotli
truapi-host 7.79 MiB 5.63 MiB 5.37 MiB
truapi-provider 4.03 MiB 1.27 MiB 959.8 KiB
truapi 5.71 MiB 804.9 KiB 659.9 KiB
Total 17.54 MiB 7.69 MiB 6.95 MiB

WebAssembly modules

Raw Gzip Brotli
truapi-host/wasm/web/truapi_server_bg.wasm 2.69 MiB 962.2 KiB 726.5 KiB
truapi-host/wasm/web/truapi_verifiable_bg.wasm 4.89 MiB 4.64 MiB 4.61 MiB
truapi-provider/truapi_provider_bg.wasm 3.99 MiB 1.26 MiB 952.1 KiB

No file changed size.

Commit: 789d871

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

CI Status: 24 required jobs green, 21 passed and 3 skipped by path filter.

All job results
job result
android-bindings success
bundle-size success
changes success
changeset-guard success
cli-package success
codegen success
e2e skipped
explorer success
headless-install success
host-android-bindings success
host-android-detekt success
host-wasm success
ios-bindings success
ios-swift success
licenses success
playground success
provider-android-bindings skipped
release-guard success
rust success
ts-client success
ts-debugger success
ts-host success
wasm-provider success
workflow-lint skipped

Signing credentials: failure as of 2026-10-02, a release may fail

Commit 789d8710 · run log

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

iOS simulator preview

Built from ef091824a, stamped with it in TrUAPICommit.

gh run download 36868114828 --name simulator-preview-ef091824a
unzip polkadot-app-*.app.zip
xcrun simctl install booted polkadot-app.app
xcrun simctl launch booted io.parity.polkadotapp.develop

Or download it in a browser, which arrives as a zip wrapping
the .app.zip, so it needs unzipping twice.

An arm64 simulator slice, so it needs an Apple Silicon Mac and does not
install on a device. Kept for 14 days, after which the link stops
resolving and a new push rebuilds it.

Comment thread rust/crates/truapi/src/store/observe.rs Outdated
Comment thread rust/crates/truapi/src/store/observe.rs Outdated
Comment thread rust/crates/truapi/src/store/observe.rs Outdated
Comment thread rust/crates/truapi/src/store/observe.rs
valentunn and others added 3 commits October 2, 2026 12:27
A dropped stream's observer stayed registered until the next write to one
of its tables or the database closing, keeping its channel and the
waiting task's waker alive. The stream now owns a `Registration` whose
`Drop` unregisters the observer, and that is the only place observers are
removed: `publish` and `close` only wake them.

Review feedback also renames the shared authorizer to
`authorize_for_change_tracking` and inlines the schema-tables SQL into
`schema_tables`.

Refs #964

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An observed query could run its statement several times, appending every
run's rows to one flat snapshot. Two different results could then record
the same rows: buckets `(["a"], ["b"])` and `([], ["a", "b"])` both
snapshot `["a", "b"]`, so dedupe dropped a real change.

The query closure now takes `ObservedStatement` by value, and
`query_map`, `query_optional` and `query_row` consume it, so a second run
does not compile. Reading several parameter sets takes one observer each,
each with its own snapshot. The snapshot is owned by the refresh and lent
to the statement.

Refs #964

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@valentunn
valentunn enabled auto-merge October 2, 2026 09:58
@valentunn
valentunn added this pull request to the merge queue Oct 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Oct 2, 2026
@valentunn
valentunn enabled auto-merge October 2, 2026 13:06
@valentunn
valentunn added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit f7313d8 Oct 2, 2026
42 checks passed
@valentunn
valentunn deleted the feat/store-observation branch October 2, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changeset No published artifact changes; changeset not required rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants