Expand description
§rindle — an incremental view maintenance (IVM) engine
Build a query, hydrate its result, and push source changes to update that result incrementally. This crate provides the raw graph, query builder, memory source, and optional WebAssembly bindings. It is based on Zero’s ZQL dataflow engine.
Applications that need SQLite persistence and automatic SQL change capture should
start with rindle-replica. Custom SQLite sources live in the separate
rindle-sqlite crate. This core crate does not link SQLite or require a C toolchain
with its default features.
§Public entry points
- Build a query —
Query/table(fluent) or anAst(Zero’s wire JSON, viaserde), thenbuild_pipelineto lower it into a wired arenaGraph. - Run it — hydrate the
Graphover aMemorySource, pushSourceChanges, and read the maintainedView/ViewData. Arindle-sqlite::TableSourcecan supply a SQLite leaf. - Import core types — use
rindle::graph::{Graph, NodeId},rindle::change::SourceChange, andrindle::value::{OwnedRow, OwnedValue}. The fluenttablebuilder is re-exported at the crate root. - Errors — fallible paths return
RindleError(see Fault model below).
§Feature flags
memory is enabled by default and required by this crate. There is no sqlite
feature; use the rindle-sqlite crate for that backend. serde enables AST
serialization, and wasm adds the JavaScript bindings plus memory and serde.
observe enables tracing, metrics enables process counters, and testkit
exposes the test oracle. fast-alloc opts into mimalloc and its native toolchain.
§Design
The arena + NodeId operator graph, RAII (Drop) cursor cleanup, the COW
Arc<Node> B+tree, and the reentrant-fetch-during-push thesis that motivates them
are written up in docs/DESIGN.md.
§Fault model (productionization WS02)
The engine is built to be survivable in a server: no data-reachable input or transient backend error should abort the process or silently corrupt view state.
- Use the
try_*entry points in production —Graph::try_source_push,Graph::try_hydrate,Graph::try_add_source— which returnRindleError. Their infallible peers (Graph::source_push/Graph::hydrate/Graph::add_source).expectthe result and are for tests/prototyping. - Strict change validation (
Graph::set_validate_changes, WS02.2): off by default (hot-path parity). When on, a malformed change stream (ADD of an existing row, REMOVE/EDIT of an absent row) returnsRindleError::ConsistencyViolation— in release, not a strippeddebug_assert!. - Ingest validation (WS02.4):
Graph::try_add_sourcerejects a wrong-width row withRindleError::SchemaViolationbefore it reaches an unchecked index. - Predicates never crash on a column/literal type mismatch (WS02.4).
- SQLite operator-storage errors (the spill backend) park a
RindleErroron the graph’s runtime-error sink and surface viatake_runtime_errorinstead of.expect-aborting; spilled blobs carry a format-version byte and a mismatch is a typed error (rebuild-from-source), not a panic (WS02.3 / WS09.3). - Panic taxonomy (the
invariantmodule, WS02.1): every remaining panic is a build-time / internal invariant; data-reachable failures areRindleError. - Server fault isolation (WS02.5): build the server with
--profile release-server(panic = "unwind") and drive mutations throughGraph::source_push_isolatedto contain a residual panic to one mutation (then discard + rebuild the view — never reuse a torn one). The wasm client keepspanic = "abort"plus a console panic hook.
§Client (wasm/JS)
With the wasm feature, wasm::RindleView drives the engine from JavaScript over a
5-call lifecycle (the wasm module has the full rustdoc):
RindleView.build(astJson, schemas, data)— lower an AST (Zero’s wire JSON) + a{ table: SchemaSpec }map (+ optional{ table: row[][] }initial data), hydrate, return a handle.BuildError/RindleErrorthrow as a JSErrorwith a.kindtag — never a wasm abort.view.data()— the materialized tree as a JS value:{ <col>: v, …, <relName>: [child…] | child | null }, in-view relationships only.view.push(table, { type: 'add'|'remove'|'edit', row, old? })— one source change (does not flush).view.flush()— notify subscribers if the view changed. This is a notification boundary, not a durable SQL transaction.view.subscribe((data, resultType) => …)— fired once immediately, then when a changed view flushes or its result type changes. Release the generated JS handle with.free()when finished.
JavaScript numbers enter the number domain; the boundary rejects bigint and
outgoing exact integers outside the JS safe-integer range. Strings stay strings;
objects and arrays become JSON cells. Build the artifact with
pnpm run build:wasm from the repository root. Concurrency: one Graph/RindleView per thread
(it is !Send); scale with N independent graphs and message passing, never a
shared Arc<Mutex<Graph>>.
Modules§
- btree
- Primitive #4 — the COW B+tree (the clean win). Spike for spec
04-sources-memory-and-btree.md§4.4 (the M3 milestone) and the two open questions it flags as the #1 implementation risk: - canon
- The canonical value-equivalence key (
CanonVal/CanonKey) — the join family’s grouping key, the limiters’ partition identity, and a parameterized query family’s binding (design 310, D1). Lives inrindle-value(below the engine, sorindle-wirecan name it) and is re-exported here as therindle::canonnamespace. The canonical value-equivalence key —CanonVal/CanonKey— oneHash + Eqencoding of the engine’s exact equality class, shared by every consumer that must agree on “these two cells are the same value”: - change
- The dataflow value types:
Node, the lazy relationship stream,Change,SourceChange,Constraint,FetchRequest, and the overlay snapshots. - family
- Parameterized query families (design 310 §4):
BindingSet— the binding-set handle a family root connection’s membership predicate reads — andFamilyPipeline, whatbuild_family_pipelinereturns and whatGraph::bind_family_partition/unbind_family_partition/hydrate_familydrive. Seesrc/family.rs. Parameterized query families — the engine half of design 310 (designs/310-PARAMETERIZED-QUERY-FAMILIES-DESIGN.md§4). - graph
- Primitive #1: the operator graph as an arena +
NodeId, NOTRc<RefCell<dyn Operator>>. - journal_
frame - The cross-process journal frame envelope
(
FrameHeader+FrameKind): the typed header (kind / committed_at / run identity + totals) in front of an opaque change payload wherever journal entries live — the hctree master’shct_journalpayload column and the S3 archival segments (rindle-backup). Pure bytes, no deps. Seesrc/journal_frame.rs,designs-implemented/211-HCTREE-LEADER-CDC-DESIGN.md§4.1, anddesigns-implemented/212-JOURNAL-S3-SHIPPING-DESIGN.md§2.2. The cross-process journal frame envelope — the typed header in front of every opaque change payload (designs-implemented/211-HCTREE-LEADER-CDC-DESIGN.md§4.1). - js_safe
- JS-boundary safe-integer walkers (productionization 09.8, design 226 Stage A): the
opt-in
strict_i64check that refuses an out-of-Number.MAX_SAFE_INTEGERIntcrossing to JS with a typed error instead of silently rounding it. JS-boundary safe-integer enforcement (productionization 09.8; design 226 Stage A). - metrics
- Build-gated process metrics (WS03, the
metricsfeature):metric_inc!/… fold each seam into a relaxed atomic add on a process-global registry the daemon’s Prometheus endpoint reads, and to argument-consuming no-ops (no global linked) otherwise. The scrape-path sibling ofobserve; seesrc/metrics.rs. Build-gated process metrics (themetricsfeature) — the scrape-path sibling of theobserveshim. - op
- Spec
06/07operators implemented outsidegraph.rs— the operator fan-out seam. - push_
index - The guarded push fan-out reverse index (
designs/205-GUARDED-PUSH-FANOUT-DESIGN.md): prunes a source write’s per-connection fan-out to the connections whose equality-shapedwhereguard could match. A conservative superset index — never under-approximates — sofilter_pushstays the exact gate. Guarded push fan-out: a reverse predicate index over source connections (designs/205-GUARDED-PUSH-FANOUT-DESIGN.md). - source_
common - Backend-agnostic source machinery, shared by the memory (
BTree) and SQLite leaves. This module references noBTree, norusqlite, AND noNode— it is generic overS: RowStreamand deals only in rows (OwnedRow) and source changes (SourceChange). It is the seam the spec callssource_common(04§4.6): the two leaves differ only in the concreteSthey pass in. - storage
- Spec
10— operator scratch state: the small, sorted, string-keyed side-table a stateful operator (Take,Cap) keeps bookkeeping in acrossfetch/pushcalls. Portszql/src/ivm/operator.ts:132Storageandzql/src/ivm/memory-storage.ts. - value
- The value vocabulary — moved to the dependency-free
rindle-valuecrate and re-exported here, sorindle::value::…is unchanged for every consumer. Crates that need only cells (the write plane, the CDC apply plane) depend onrindle-valuedirectly and therefore cannot reach the engine at all. The canonical value/row model — the union that graduates into01-foundations§2–§3, refined by the SQLite step-buffer lifetime (the forcing function). One model serves BOTH leaf backends:
Structs§
- Ast
- The query AST (
Ast,ast.ts:217-243).tableis the only required field; every other wire field is optional.Defaultenables struct-update syntax for hand-written test expectations. Deserializes from the JS wire JSON (camelCase keys; absent ⇒None/empty). - Batch
- One transaction’s flat changes (or the seq-0 hydrate snapshot).
eventsapply in order; the receiver renders only after the whole batch (FLAT-CHANGES-DESIGN.md§5.4). - Bound
- A paging lower bound (
Bound,ast.ts:199-202).rowis a partial wire row — the bound columns by name (aBTreeMapfor deterministic key order, matching the wire object).exclusivemaps to the runtimeBasis(false⇒At/inclusive,true⇒After/exclusive) at lowering, in the builder/Skip(spec03§3.5). - Caught
Node - A fully-materialized node: the row cells plus eagerly drained
relationships. The comparison unit for a fetch/push assertion, and the owned
node a
Graph::add_change_sinkconsumer receives. - Cond
- A boolean condition group under construction — the building block for
nested
AND/ORfilter trees. You don’t make one directly; a freshCondis handed to the closure ofQuery::where_any/Query::where_all(and the nestedCond::any/Cond::all). Add clauses by chaining, just likeQuery. - Correlated
Subquery - A child query joined to its parent by a key
Correlation(CorrelatedSubquery,ast.ts:250-265). Used both as a materialized relationship (Ast::related) and inside aCorrelatedSubqueryCondition. - Correlated
Subquery Condition - A
(NOT) EXISTScondition (CorrelatedSubqueryCondition,ast.ts:324-331). - Correlation
- The join key pair (
Correlation,ast.ts:245-248).parent_field[i]on the parent correlates withchild_field[i]on the child; both non-empty, same length. - Entry
- One materialized node in the view tree (ports
MetaEntry).Arc-shared so unchanged subtrees keep pointer identity across an immutable [apply_change]. - Exists
Opts - Options for a
where_exists/where_not_existscorrelated subquery — an extensible struct so the common case stayswhere_exists(f)and opt-in behaviors ride a..Default::default()struct via the_withvariants. - Flat
Change - One flat change: the root→leaf parent path, then the op applied at the reached
level. An empty
pathmeans the op applies at the top level. - Hello
- The subscription handshake, sent once before any
Batch. - Like
Matcher - A compiled SQL
LIKEpattern (07§4 /filter.ts).%matches any byte run,_matches exactly one byte. Compiled once (the builder lowers the literal pattern) and matched per row with a classic backtracking glob walk. - Memory
Source - The in-memory source.
fetchseeks aBTreeCursorover a COW snapshot and drives it throughsource_common;write_changepath-copies viaRc::make_mutso in-flight cursors keep their snapshot (the load-bearing COW property — §6). - Order
Part - One
(field, direction)of an ordering (OrderPart,ast.ts:208). A wire 2-tuple — serializes as the JSON array["field", "asc"]. - Parent
- A reference to a parent-row column (see
ParentRow::col). As awherevalue it defines aCorrelation, not a filter. - Parent
Row - A handle to the parent row, handed to a
sub/where_existsclosure.row.col("creatorID")yields aParentreference to that parent column. - PathSeg
- One hop of a
FlatChangepath: descend relationshiprel, locating the parent at the current level byparent_row(the full sort-key locator). - Publisher
- Sender side: stamps batches with the subscription
epoch+schema_fpand drives the gap-free seq. Graph-agnostic — the caller drains the change-sink and hands theCaughtChanges here. - Query
- The fluent builder. Holds the
Astunder construction plus a pending correlation — the(child, parent)field pairs siphoned fromwhere(child, row.col(parent))calls, which the enclosingsub/where_existsdrains. - Receiver
- The reference receiver: holds the hierarchical view
Schema(the shipped query schema) and the reconstructed top-level list (root[""]). - Recv
Node - A reconstructed node: row + multi-path refcount + per-slot child lists.
- Simple
Condition - A single comparison (
SimpleCondition,ast.ts:302-312).rightis wire-typed to exclude a column (Exclude<ValuePosition, ColumnReference>); the builder validates “not a column” at its boundary. - Snapshot
Chunk - One frame of a chunked hydrate snapshot (
FLAT-CHANGES-DESIGN.md§5.4). The initial snapshot isO(full result)— the whole tree as top-levelAdds (each with its inline subtree) — so it is paged acrossSnapshotChunks rather than one giant batch.addsis a slice of those top-levelAdds;indexis gap-free within the snapshot;lastis thesnapshot_completemarker (the receiver renders only after it). All chunks carry the subscriptionepoch+schema_fpso a mid-snapshot drift is caught. A chunk’saddsapply atomically (each carries its full subtree). - Subscriber
- Receiver side: a
Receiverplus the protocol state, enforcing the §2.3/§5.4 rules. The baseline is established by either a single-shotapplyof the seq-0 snapshot batch or a sequence ofapply_snapshot_chunkcalls; then incremental batches flow. - View
- The materialization sink (ports
ArrayView). Lives in the arena asOperator::View; its graph-touching halves (hydrate/view_push) areGraphmethods that delegate the pure folding to [apply_change]. - Wire
Node - A materialized node on the wire: its row plus slot-keyed, pre-sorted child
subtrees — the wire-shaped twin of
CaughtNode(Vecrows; relationships as a sortedVec<(slot, children)>rather than aBTreeMap). - WireRel
- A relationship slot on a
WireSchema: its name, its slot index, either the child level’s schema (in-view) orNone(gating / out-of-view — the receiver’s in-view gate dropsChildchanges addressed at it,FLAT-CHANGES-DESIGN.md§6), and an optional scalar-projection annotation (REDUCE-DESIGN.md§9). - Wire
Schema - One level of the hierarchical view schema, wire-shaped. See the module docs.
Enums§
- Aggregate
- An aggregate over a (correlated) subquery’s rows (
REDUCE-DESIGN.md). - Applied
- The outcome of applying a
Batch. - Build
Error - Errors raised while lowering an
Astinto pipeline pieces (spec08). Introduced forcreate_predicate; thebuild_pipelinespine grows it. - Caught
Change - A caught downstream change. Mirrors
catch.tsexpandChangeoutput: anEditcarries only the two rows (no node,catch.ts:104-109); aChildcarries the parent row, the relationship slot, and the nested change (catch.ts:110-118). - Condition
- The filter tree (
Condition,ast.ts:296-300). Wire-tagged by"type"; recursive through theVecs (heap-boxed elements) and the boxed subquery. - Dir
- Sort direction for an
OrderPart. Wire'asc' | 'desc'(ast.ts:24). - Exists
Op 'EXISTS' | 'NOT EXISTS'(CorrelatedSubqueryConditionOperator,ast.ts:333).- FlatOp
- The op at the end of a flat change’s path (the reached level).
- Lit
- An AST literal value (
LiteralValue,ast.ts:284-289). Its own type — distinct from the runtimecrate::value::OwnedValue— so the AST derivesPartialEq(OwnedValueforbids derived comparison; you must pickcompare_valuesvsvalues_equal). The two are bridged at lowering time (builder,08). - Op
- A simple comparison operator — the wire
SimpleOperatorset (ast.ts:211-215). Serializes as the exact SQL-ish string ("=","!=","IS NOT","NOT IN", …). - Protocol
Error - A protocol violation a
Subscribersurfaces. All but a duplicate are fatal to the current subscription — the consumer must discard its tree and re-subscribe (the sender re-hydrates under a new epoch). - Result
Type - The query’s completion state (
typed-view.ts). - Rindle
Error - Snap
Status - The outcome of applying a
SnapshotChunk. - System
- Subquery provenance (
System,ast.ts:28). Shared with the runtime layer. - Value
Position - A value position in a
SimpleCondition— a column reference or a literal (ValuePosition,ast.ts:267, minus the deprecatedstaticparameter form). Wire-tagged by"type".
Constants§
- COMPARATOR_
VERSION - The
compare_values/compare_rowsalgorithm-contract version (§4/§5.5). Bump this whenever the total order changes (null handling, float/total_cmp, the bytewiseBINARYstring order, cross-type rules). A receiver MUST refuse a subscription whosecomparator_versiondiffers — its reconstruction would silently corrupt.
Traits§
- Scalar
Catalog - Maps a table name to its
ScalarSource. The resolver only ever looks up the child (subquery) table — the parent is never read. - Scalar
Source - The per-table read seam the resolver uses to fold a statically-unique subquery.
One impl per source backend;
MemorySourceprovides the PK-only slice impl.
Functions§
- build_
family_ pipeline - Lower a parameterized query family (design 310 §4) into
graph: one pipeline over the family’sstrippedtemplate (rindle-wire’sFamilyTemplate::stripped— the number-canonicalized AST with its holed root-equality conjuncts removed) whoseparamscolumns are a partition dimension. Compared withbuild_pipelineover a concrete member, the compiled pipeline differs in exactly three places: - build_
pipeline - Lower an
Astinto a wired pipeline ingraph, returning the top operator (the one a sink attaches to viaGraph::set_sink_edge). The port of JSbuildPipeline/buildPipelineInternal(builder.ts:256) for the built operator subset: a source connection carrying its pushed-downwhere(ConnectionFilters) plus a chain of parent-driven relationship joins. - canon_
of_ lit - The binding-value class of a scalar literal (design 310 §3.1 / impl plan §3.2):
Someifflitis a scalar a parameterized query family may bind on —Bool/Int/Strdirectly,Numberthrough the same number coercion the predicate lowering applies (lit_to_scalar→number_to_owned) and thenCanonVal::of, soInt(1)andNumber(1.0)are one binding exactly whenvalues_equalsays so, and a binding agrees cell-for-cell with thecol = litpredicate it stands in for.Null(SQL never-match — the predicate folds it tofalse),Array(anINlist), and a non-finiteNumber(no wire token; [canonicalize_wire_number_lits] folds it toNull) areNone— ineligible. - flatten
- Linearize one
CaughtChangeinto aFlatChange: peel eachCaughtChange::Childinto aPathSeguntil a non-Childop is reached. - flatten_
all - Linearize a batch of
CaughtChanges (e.g. one transaction’s drained sink), preserving order. The order is significant — the receiver must apply the resulting flat changes in exactly this order (FLAT-CHANGES-DESIGN.md§5.4). - has_
scalar_ subquery - True if
astcontains anyscalar-flagged correlated subquery (anywhere in itsWHEREtree or a nested/relatedsubquery). A cheap gate so a caller can skip theresolve_scalarsclone when there is nothing to fold — the peer of the planner’shas_flippable_exists. - resolve_
scalars - Resolve every
scalar-flagged correlated subquery inast(itsWHEREtree and, recursively, every nested subquery andrelatedchild) againstcatalog, returning a rewritten AST in which each fold has replaced itsEXISTS/NOT EXISTScondition with a plainSimplecondition (so the builder emits no join for it). Conditions without the flag are untouched, so an AST with noscalar: trueanywhere round-trips unchanged. - schema_
fp - Fingerprint a resolved
WireSchema. The PK andsortare hashed by column name (resolved throughcolumns), so the fingerprint is a semantic identity independent of any internalColIdnumbering (§5.5). - table
- Start a query for
table. The entry point:table("issue").select("title")…. - to_
schema - Rebuild an engine
Schemafrom aWireSchema(the receiver side). The inverse ofto_wirefor every field the receiver uses (columns, PK, sort,singular, each relationship’s name + child schema, and any scalar-projection annotation). Achild: Nonerelationship becomes a join-onlyRelDef::new(out-of-view) slot. - to_wire
- Lower an engine
Schema(the hierarchical view schema) to itsWireSchema. Recurses into each relationship’s child schema; a join-only / gating slot (no child schema) becomeschild: None. - view_
schema - Derive the production-
Viewhierarchical schema forast, resolving table names through the sameresolveclosure thatbuild_pipelineuses.build_pipelinereturns only the topNodeId, so the View’s tree shape is reconstructed here from theAst. The schema is the view shape: a relationship slot carries a child schema iff it is in view (a join-onlyRelDef::newis out of view).
Type Aliases§
- Entry
List - The reference-counted, copy-on-write child list.
- Listener
- A flush listener (
Listener,typed-view.ts:9). Fired on flush and once immediately on registration. - View
Data - The top-level result the consumer sees (
root[""]): the sorted list of root entries. AnArc-sharedEntryList, so a consumer canArc::ptr_eqthe snapshot to detect an unchanged top level. - WireRow
- A row on the wire: positional cells, aligned to the level’s
Schema.columns.