Overview
While basic/incremental watch queries enable reactive UIs by automatically re‑running queries when underlying data changes and returning updated results, they don’t specify which individual rows were modified. To get these details, you can use differential watch queries, which return a structured diff between successive query results. However, on large result sets they can be slow because they re‑run the query and compare full results (e.g., scanning ~1,000 rows to detect 1 new item). That’s why we introduced trigger‑based table diffs: a more performant approach that uses SQLite triggers to record changes on a table as they happen. This means that the overhead associated with tracking these changes overhead is more proportional to the number of rows inserted, updated, or deleted.JavaScript Only: Trigger-based table diffs are available in the JavaScript SDKs starting from:
- Web v1.26.0
- React Native v1.24.0
- Node.js v0.10.0
The
db.triggers
APIs are experimental. We’re actively seeking feedback on:- API design and developer experience
- Additional features or optimizations needed
Key differences vs. differential watch queries
- Scope: Trigger-based diffs track row-level changes on a single table. Differential watches work with arbitrary query results (including joins).
- Overhead: Trigger-based diffs do per-row work at write time (overhead grows with number of affected rows). Differential watches re-query and compare result sets on each change (overhead grows with result set size).
- Processing path: Trigger-based diffs record changes at write time and require a
writeLock
during processing (only a singlewriteLock
is allowed). Differential watches run on read connections and re-query/compare results on each change (often concurrent on some platforms). - Storage/shape: Trigger-based diffs store changes as rows in a temporary SQLite table that you can query with SQL. Differential watch diffs are exposed to app code as JS objects/arrays.
- Filtering: Trigger-based diffs can filter/skip storing diff records inside the SQLite trigger, which prevents emissions on a lower level. Differential watches query the SQLite DB on any change to the query’s dependent tables, and the changes are filtered after querying SQLite.
Trigger-based diffs
Trigger-based diffs create temporary SQLite triggers and a temporary table to record row‑level inserts, updates, and deletes as they happen. You can then query the diff table with SQL to process the changes.SQLite triggers and PowerSync viewsIn PowerSync, the tables you define in the client schema are exposed as SQLite views. The actual data is stored in underlying SQLite tables, with each row’s values encoded as JSON (commonly in a single
data
column).SQLite cannot attach triggers to INSERT/UPDATE/DELETE operations on views — triggers must target the underlying base tables. The db.triggers
API handles these details for you:- You can reference the view name in
source
; PowerSync resolves and targets the corresponding underlying table internally. - Column filters are applied by inspecting JSON changes in the underlying row and determining whether the configured columns changed.
- Diff rows can be queried as if they were real columns (not raw JSON) using the
withExtractedDiff(...)
helper.
Tracking and reacting to changes (recommended)
The primary API istrackTableDiff
. It wraps the lower-level trigger setup, automatically manages a writeLock
during processing, exposes a DIFF
table alias to join against, and cleans up when you call the returned stop()
function. Think of it as an automatic “watch” that processes diffs as they occur.
Filtering with when
The required when
parameter lets you add conditions that determine when the triggers should fire. This corresponds to a SQLite WHEN clause in the trigger body.
- Use
NEW
forINSERT
/UPDATE
andOLD
forDELETE
. - Row data is stored as JSON in the
data
column; the row identifier isid
. - Use
json_extract(NEW.data, '$.column')
orjson_extract(OLD.data, '$.column')
to reference logical columns. - Set the clause to
'TRUE'
to track all changes for a given operation.
The strings in
when
are embedded directly into the SQLite trigger creation SQL. Sanitize any user‑derived values. The sanitizeSQL
helper performs some basic sanitization; additional sanitization is recommended.Lower-level: createDiffTrigger (advanced)
Set up temporary triggers that write change operations into a temporary table you control. PrefertrackTableDiff
unless you need to manage lifecycle and locking manually (e.g., buffer diffs to process them later). Note that since the table is created as a temporary table on the SQLite write connection, it can only be accessed within operations performed inside a writeLock.