Rindle docs and package mapSkip to main content

Module normalize

Module normalize 

Source
Expand description

Normalized change events — the second serializer beside the engine’s flat path (rindle::flatten), for a local-first client that runs its own normalized base tables + local IVM.

See NORMALIZED-CHANGES-DESIGN.md for the full design. Where rindle::flatten keeps the root→leaf path and drops a remove’s subtree, NormalizeFold does the opposite: it drops the path, keeps only a table tag, and keeps the subtree on remove (so witnesses/children are decremented too, §4.3). The engine does not change — both serializers consume the same CaughtChange stream the replica already delivers (writer.rs hands out Vec<(NodeId, Vec<CaughtChange>)> per query).

Why this lives in rindle-wire, not the rindle core or rindle-replica: the sender set grew. This serializer was originally server-only — the browser receiver is the TypeScript NormalizedSync, and even in normalized mode the wasm Db is fed flat changes, never the fold — so it lived in the SQLite-linking replica. But a room (RINDLE-REALTIME-DESIGN.md §2.4) is a normalized publisher compiled to wasm, so the fold must link without SQLite and compile for wasm32 — and since the daemons and the room must speak byte-identical frames, the whole protocol (fold, envelope, JSON codec) sits in this dedicated wire crate both depend on; the replica re-exports it from here unchanged. It stays out of the rindle core for the same reason as ever: the core is the engine, not a wire protocol. (Contrast rindle::flatten, which the wasm client genuinely uses, so it earns its place in core.)

This module is Slice 1: the fold itself. It folds one transaction’s CaughtChanges into a flat per-table set with intra-query refcounts (the footprint) and emits this tick’s net membership deltas as table-tagged NormalizedOps, deduplicated so a row touched via several tree paths in one transaction collapses to one op (§4.1/§4.2). The footprint interns its rows: it holds the Arc-shared OwnedRow the producing pipeline already owns (no second copy), materializing a WireRow only for the rows it actually emits. The protocol envelope / wire framing (NormalizedPublisher, the NormalizedHello) is a later slice.

Structs§

AggTable
A synthetic aggregate base table the server ships in place of an aggregate’s child rows: the reduce’s (group_key…, value) output, given a base-table home on the client (§3.2). Columns are [child_field…, "count"] (mirroring the engine’s agg_relationship_reldef / Reduce::count_by output), the leading key_len of which are the group key (and the PK). Derived purely from the AST — there is no such table in the DB — so agg_table_schemas feeds the publisher’s hello + PK map for it.
NormalizeFold
The server-side serializer (§4): one per registered normalized query. Fold each committed transaction’s CaughtChanges with fold to get that tick’s NormalizedOps. The footprint persists across ticks; the snapshot is simply the first fold over the hydrate batch (every row 0→1 ⇒ all Adds).
TableNode
The query’s table tree: each frame’s base table plus, per relationship slot (in the query-local RelId order), the child frame’s tree. It is the path-free replacement for PathSeg: folding a CaughtChange::Child descends children[rel] to learn the child’s table, then throws the parent row away.

Enums§

NormalizedOp
One normalized change. Self-identifies by table name — the client routes each row to a base table directly, with no tree path and no slot map (§3). Rows are positional (aligned to that table’s column order). OwnedValue is serializable because the crate enables rindle’s serde feature, so the op serializes for the wire / oracle directly.
ReqCols
What columns this query needs synced for a base table. All ⇒ a '*' frame (no select) referenced the table, so it must sync every column; Names ⇒ the explicit set of column names the query structurally reads for the table (its required_cols at the name level — selectwhere-leaf ∪ order_bystart ∪ correlation fields). Unioned across every frame that references the same table (any All wins).

Functions§

agg_table_name
The synthetic base-table NAME for a relationship count aggregate (§3.1): a content hash of the aggregate’s definition — child table, kind, the group key (correlation child fields), and the child where filter — so two queries with the same definition share one table (cross-query refcount on the client) while a different filter gets a different table (no (table, pk) collision). The parent correlation field is excluded: the count for a given group key is the same whichever parent joins it.
agg_table_schemas
Collect a synthetic AggTable for every relationship count aggregate in ast, recursively (a nested aggregate under a materialized related is included). The caller (the replica consumer / the publisher’s schema list) advertises these alongside the real base-table schemas so the client can register + validate them.
reject_unsupported_sync_aggregate
Reject a relationship sum/avg aggregate anywhere in ast’s related tree — the precomputed-sync path (synthetic aggregate base tables, AGGREGATE-SYNC-DESIGN.md §3) is count-only: it ships the reduce’s (group…, count) row as a base table and has no wire encoding for a sum/avg value or its int-vs-real typing. A relationship sum/avg still works for a direct engine read (the generic scalar projection); it is only unsupported for sync materialization, so registration surfaces it as a [BuildError] rather than silently minting a count-shaped synthetic table. Call this at each sync-registration entry (the replica’s register_shared_query, the room’s materialize) before any of table_tree / agg_table_schemas / NormalizeFold walk the tree — that ordering is what makes the Sum/Avg arm of hash_agg_subquery unreachable.
required_columns_by_table
The columns each base table in ast’s footprint needs synced (§5.2). A pure function of the AST (column names, resolved to indices by the caller against the table schema). Mirrors table_tree’s slot walk + prune (exists_noSync slots are never synced, so they contribute nothing). The result drives the projected wire schema and project-at-emit.
rewrite_aggregates
Rewrite an AST for a client engine reading synced aggregate tables — the Rust twin of TypeScript rewriteAggregates (packages/normalized/src/agg-table.ts), byte-for-byte in its choice of table name because both call agg_table_name.
rewrite_aggregates_with_local
rewrite_aggregates, with the L1 local-table carve-out (201-LOCAL-ONLY-TABLES-DESIGN.md §5.2): a count over a table is_local accepts is a native IVM reduce with no server-authoritative __agg_* base, so it is left alone — rewriting it would point the relationship at a synthetic table nothing ever feeds.
table_tree
Derive the TableNode tree for ast. A pure function of the AST — it needs only table names (all present inline: the root ast.table, each subquery’s table), never the source schemas, so no resolve closure. Mirrors view_schema’s slot walk exactly (normalize the frame, take [query_local_slot_names], resolve each slot name to its subquery, recurse) but carries the table where the schema carries the alias.

Type Aliases§

PkMap
Table → primary-key column indices (into that table’s row), supplied at construction.