You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
RFC: Stabilizing includes / nested materialization
Status: production implementation complete in PR #1740; awaiting merge Scope: bug fixes and internal refactors only — no new public API surface, no behavior changes beyond fixing verified bugs. Working model: one coherent D2 graph implementation, backed by direct oracle assertions and narrow boundary adapters.
Reusable trace runner merged in test(db): extract reusable trace runner #1718: driver/projection split, same-turn checkpoints, cleanup-error preservation, synchronous assertion typing, and a full-row batch driver.
The expanded scalar-materialization driver found a new deterministic stale nested-reference failure; its reduced expected-failure seed landed in test(db): extract reusable trace runner #1718.
State-equivalence coverage expansion merged in test(db): expand includes oracle state coverage #1719: generated full-row batches at depths 1–4, guaranteed connected paths, reorder and single-row rekey/reparent coverage, flat array/concat projections, and three reduced expected-failure classes.
State-aware relationship-transition fuzzing merged in test(db): fuzz visible includes relationship transitions #1722: transition-only and stateful paired cases across every depth/target level, connected scalar activity, explicit key-separation invariants, and reduced expected-failure traces for every observed rekey and reparented-subtree boundary.
Audited every issue and PR opened since this RFC on 2026-07-08; added the relevant reports below and recorded adjacent work owned by other RFCs.
Layered-publication oracle merged in test(db): cover layered includes publication #1736: two sibling materializations across direct/joined Q1 and pass-through/where/orderBy/select Q2 shapes, including synced and optimistic publication, recovery, rollback, and replacement histories.
PR feat(db): rebuild includes materialization as one D2 graph #1740 replaces the legacy includes materializer with one D2 graph, opaque source identities, weighted public-key reduction, recursive materialization, coherent facade/root publication, and generation-scoped subset demand.
Every includes expected-failure guard is removed; the known defect catalog now passes as direct production assertions.
Post-fix oracle expansion unfreezes correlation keys, checks Collection/array/materialized forms together, adds seed replay and generated-distribution diagnostics, and adds cross-formulation nested/flat/per-parent/TLP equivalence.
A 100x campaign ran every property with no semantic divergence; the normal eight-file gate passes 219/219 with no type errors.
The detailed issue evidence below records the pre-implementation state. Section 3 and live GitHub issue status are authoritative for the completed implementation.
1. What's happening
The includes system (subquery-in-select, toArray(), materialize()) has produced a steady
stream of correctness bugs: silently misrouted data, dropped children, stale sort order, broken
adapter reactivity, and permanent loading states. Every claim below was verified against current main with a red/green test (tests live on this branch, in describe('cluster-verification …')
blocks appended to existing test files).
orderBy in an include ignored after optimistic update on the child collection
Fixed on current main: the exact optimistic child-reorder regression is green and retained in the recompute oracle; #1444 closed and PR #1496 closed unmerged as superseded
Progressive sync: nested toArray children skip the fast-path snapshot
RED — confirmed: lazy alias ⇒ includeInitialState: false ⇒ the only requestSnapshot fires per parent row, after the progressive buffering window closed
same file, #1533 block (paired passing baseline for the direct query)
Initially-empty include starts null and never becomes reactive
Not reproduced: field is an empty child Collection from first render and populates on insert (caveat: Collection instances aren't Solid-reactive by design)
On-demand observer reuse loses row ownership; cleanup deletes rows still in use
Not reproducible on main: the early-return shape exists, but atomic observer+ownership cleanup and ownership re-registration on subscribers:change compensate; likely fixed since the reported version
innerJoin drops a live-query result when one of many children is deleted #1703 — deleting one of several matching joined rows deletes the collapsed live-query row.
This is a P2 multiplicity failure outside the include wrapper: several differential rows can map
to one public key, so the reducer must retain net multiplicity and publish a delete only when the
count crosses zero. Add a direct join driver and reduced expected-failure trace before PR 2.
3-level nested materialize is dropped when the middle level is shared by more than one parent row #1685 / PR Fix #1685 #1686 — three nested materialize(findOne()) levels drop the deepest row for one
root when distinct middle rows point to the same shared row. RED — confirmed: the first
recursive consumer deletes the shared row's INCLUDES_ROUTING stamp, so the second cannot
register the next routing edge. This is another P3 shared-routing failure. PR Fix #1685 #1686 defers stamp
cleanup until the outermost flush; land it as a narrow stopgap with the exact scalar regression,
then delete that lifetime bookkeeping in PR 3.
Discovered by test(db): extract reusable trace runner #1718 — nested scalar reference replacement stays stale. Updating a middle
row from sharedId: 1 to a missing sharedId: 2 updates the visible foreign key but retains the
old shared row in the existing incremental query. A fresh query over the same collections matches
recomputation, isolating the fault to incremental nested routing/materialization. The reduced
five-step expected-failure seed now lives in the scalar-materialization driver. Treat this as
another P3 routing-lifetime failure, not an isolated symptom fix.
Discovered by test(db): expand includes oracle state coverage #1719 — reinserted parents retain an obsolete shared route. When two parents share a correlation key, deleting and reinserting one under a new key can leave the old route alive; a later child for the old key is materialized under the reinserted parent. The reduced expected-failure trace isolates another P3 route-lifetime failure.
Discovered by test(db): expand includes oracle state coverage #1719 — intra-batch child hand-off leaves aggregate membership stale. When a full-row batch moves multiple children between correlation buckets, array materialization and concat(toArray(...)) can both retain old membership instead of applying the whole hand-off. These are two projections of one batch-routing defect, covered by one reduced scenario under both materializations.
Discovered by test(db): fuzz visible includes relationship transitions #1722 — rekey detachment and moved-subtree reactivity share a two-descendant boundary. Rekeying a visible row diverges immediately when at least two included levels remain below it. Reparenting is initially correct, but sequential scalar updates through the next two descendant levels leave deeper values stale. Reduced traces cover rekey at depth/level (3,1), (4,1), and (4,2), plus the corresponding three reparent/update-chain shapes; generated post-transition activity stays on the unaffected branch until the runtime fix turns those traces green.
Discovered by test(db): fuzz includes route lifecycles #1734 — a root entering an already-live shared route misses that route's existing child snapshot. The failure is limited to root-level entrants; the same merge at nested parent levels is green. The classifier requires the incremental result to differ from recomputation by exactly the missing ordered subtree, with an adjacent two-child case proving the whole route snapshot is omitted.
Discovered by test(db): fuzz includes route lifecycles #1734 — restoring a retired root route leaks its updated snapshot to a departed root. Starting from a shared root route, both subscribers leave, the detached child route changes, and one root later resubscribes. Incremental output attaches the restored child snapshot to the root that remains departed; recomputation attaches it only to the returning root. The exact expected-failure checkpoint is root-only; equivalent nested-level histories remain green controls.
Poor performance for nested includes #1634 — useLiveQuery with multi-level includes costs up to ~100ms per run. Open PR test(db): add includes work and shape oracles #1738 adds a clock-free semantic gate for the report's 20×2×5×10 tree. All 20/40/200/2,000 source rows must be delivered by roots.preload(), traversing the public result must cause no further source delivery, and the reachable result must expose 20/40/200 child collections containing 40/200/2,000 rows. The public API cannot count internal allocations or discarded intermediate collections, so PR 3 still needs a benchmark and production-boundary instrumentation for construction-cost claims.
Reviewed but owned elsewhere:#1662 is already evidence E13 in loadSubset RFC #1657; #1698 is
an Electric/sync ingestion defect under RFC #1659; #1708 is a general join-index selection issue; #1712 asks for a new remote-join feature; and #1721 changes React hook state ownership under the
live-query platform work. They are not gates for this includes RFC.
Status audit (2026-08-17):#1716–#1739 are merged. PR #1740 contains the production graph rewrite and the final oracle expansion. Its directly owned gates are green; related adapter, ownership, and performance reports remain scoped as stated in the PR body.
Why these keep happening
The bugs are not independent. The includes system compiles each include into its own child D2
pipeline (sound), but then reconstructs include semantics in a ~2,300-line imperative output layer
(packages/db/src/query/live/collection-config-builder.ts) using alias maps, child collection
registries, correlation routing indexes, pending-change buffers, and in-place parent-row mutation.
Correctness rests on identities that are only implicit:
Object identity is not result revision.flushIncludesState mutates parent rows in place
and force-emits through changesManager.emitEvents(events, true) to defeat the collection's own deepEquals suppression. React's version-bump mostly tolerates this; Solid's reconcile does
not (Include value updates break with solidjs #1571), Nested includes are undefined on next render after collection.update #1635 suggests React has its own window, and each future adapter needs its own
workaround.
The completed oracle suite keeps the five original principles but changes their boundaries:
Principle
Decision
Revised boundary
P1 — Opaque plan identities
Keep
Land first and independently. Use source, relation-node, include-node, and materialization-edge IDs; aliases remain lexical names and debug labels.
P2 — One transition reducer
Strengthen
Use a persistent contribution ledger. Public Collection state cannot safely stand in for contributor multiplicity across batches.
P3 — Correlated relation
Decompose
Separate ordered bucket contents, subscriber routing, and route demand. Root and nested materialization cells use the same protocol.
P4 — Publication by replacement
Keep, move upstream
Compose immutable, fully materialized rows before the public Collection commit. Observers are consumers, not a repair boundary.
P5 — Demand-relative readiness
Keep
Acquire and retire demand leases through the same route-lifecycle operations that manage subscribers.
The target data path is:
ID-keyed compiled D2 plan
|
v
weighted contribution deltas
|
v
ContributionLedger
|
v
ordered root/bucket relations
|
+-------- RouteLedger -------- DemandRegistry
| |
v v
dirty materialization cells
|
v
immutable MaterializationComposer
|
v
one coherent Collection commit
|
v
downstream live queries and adapters
CorrelatedRelation is therefore a protocol implemented by several small stateful components, not one object that owns contents, subscribers, demand, and publication. Rebuilding all of those responsibilities inside one operator would recreate CollectionConfigBuilder under a new name.
Seven architecture axes
Compiled identity graph. The compiler assigns branded internal IDs to sources, relation nodes, include nodes, and materialization edges. Runtime maps are ID-keyed. Aliases resolve only inside their lexical query scope. A bucket is identified by an include-node ID plus a canonical structural correlation tuple, never by a flattened alias or JSON.stringify of mutable context.
Persistent contribution accounting. Each public key retains its weighted internal contributors across batches. A graph run applies every delta, compares the aggregate before and after, then emits at most one visible set/delete transition. Root and include output use the same ledger.
Ordered bucket relations. Correlated child contents live in lightweight ordered stores with stable keys and monotonically increasing revisions. Relation contents do not own subscribers or demand.
Route/subscriber lifecycle. A subscriber is a materialization cell: output relation ID + parent public key + materialization edge ID. Subscribe returns the exact current bucket snapshot; unsubscribe removes only that edge; move is atomic; generations reject stale updates and snapshots. Roots and nested rows use this same protocol.
Coherent immutable publication. A pure composer creates new values only along changed paths. A private materialization commit stages base-row, bucket, route, composition, delete, and order changes. The public Collection receives one normal commit only after every affected row is fully materialized.
Route-coupled demand. Lazy and progressive subsets are represented by generation-scoped leases. Readiness means every active lease in the current route graph has settled. Empty outer results create no child leases; retired generations cannot publish or settle current demand.
Independent ownership and physical work. Query-db row ownership belongs in a typed ownership ledger, not the includes materializer. Correlated-plan work and internal footprint have their own counters and acceptance criteria rather than being inferred from correct results.
Stable defect catalog
The expected-failure tests use exact classifiers and checkpoints. The RFC names each class so it remains visible outside test code:
The optimistic suite does not define a separate architecture axis: its only relationship failure is the existing deep-rekey class reached through another producer. Optimistic changes remain weighted inputs to the same ledger and route protocol.
The batch suite also argues against replacing the scheduler or D2 graph-run boundary. Atomic delivery, reverse order, same-ID replacement, and fresh-route mirrors are green. The narrow split/new-ID/handed-off-route defect belongs in route ownership and staged application.
Normative laws
These are permanent contracts even if component names change:
Alpha-renaming: lexical alias names cannot change results.
Contribution conservation: a public row exists iff its net supporting contribution is positive.
Batch partition: equivalent valid split and atomic deliveries converge.
Subscription: subscribe receives the current snapshot exactly once; unsubscribe receives no later changes.
Route generation: a retired generation can neither publish nor settle demand.
Publication: an event payload, synchronous Collection read, and downstream live query observe the same fully materialized revision.
Demand: readiness is equivalent to settlement of the active demand graph.
Ownership: a row exists iff at least one explicit owner token remains.
Work: irrelevant correlated rows do not increase examined work on an indexed physical path.
Space: internal state scales with relations, routes, edges, and visible rows—not recursive full Collection machinery.
3. Implementation outcome
PR #1740 implements the architecture as one coherent change rather than the earlier staged cutover. The landed oracle work made that safe: every known defect has an exact checkpoint or structural classifier, and the production branch removes those classifiers instead of weakening them.
The runtime now uses opaque compiled source identity and D2 weighted relations for contribution, route, ordering, and nested propagation. BucketFacadeAdapter owns only public Collection identity and subscription. SubsetDemandController owns only asynchronous demand coverage, cancellation, and readiness. Coherent publication installs child facades and root rows before observers run. The legacy alias maps, route registries, reverse indexes, recursive internal Collections, depth buffers, and imperative snapshot drains are removed.
The final testing strategy combines deterministic regression traces, 172 exhaustive micro-domain cases, random route-changing FastCheck histories, Collection/array/materialized comparison, scheduled demand interleavings, and cross-formulation nested/flat/per-parent/TLP equivalence. TANSTACK_DB_ORACLE_SEED replays random campaigns, TANSTACK_DB_ORACLE_RUNS_MULTIPLIER scales them, and TANSTACK_DB_ORACLE_STATISTICS=1 reports generated coverage. The default eight-file gate passes 219/219. A 100x run completed all assertions without a semantic counterexample; Vitest emitted one post-run worker-RPC timeout, after which the normal gate exited cleanly.
Generic DBSP laws are deliberately separate because they belong to @tanstack/db-ivm, not the includes facade. Follow-up #1741 tracks Q(x + Δ) = Q(x) + Q^Δ(x, Δ) coverage for core differential operators.
4. Non-goals / rejected approaches
No new public APIs (explain, loading-status fields, demand/lease surface, new helpers).
No alias mangling (beyond the fix: duplicate alias in sibling includes silently breaks nested children #1455 fallback, if taken), no additional per-depth buffers or
flush sub-passes, no per-adapter cloning beyond the temporary A2 shim, no growing the
readiness-exclusion list beyond A1's stopgap. Each of these closes one issue while making the
state machine harder to reason about — PRs 2–5 exist to delete them.
5. Risks
The change is large, so the direct oracle gate, fixed regression corpus, random replay controls, and full DB/adapter suites are merge gates.
Collection facades and lazy demand remain stateful boundaries. Tests require them not to recreate relation state or publish partial revisions.
LoadSubsetOptions.signal is optional for compatibility; adapters that fetch asynchronously should honor it before installing rows.
Nightly high-run CI and a full Stryker audit are not part of this PR. The multiplier, replay seed, and focused mutation audit make those follow-ups possible.
Issue #1488's reported observer-reuse path does not reproduce on current main, as pinned by #1737. The same oracle reproduces #1631 and PR #1656 through different ownership-loss mechanisms, so the ownership/refcount lifecycle remains a required gate rather than being dismissed with #1488's green result.
Issue #1505 is closed; its underlying concern (include fields transiently unmaterialized, types
don't admit it) is addressed by PR 4's always-attached include values.
RFC: Stabilizing includes / nested materialization
Status: production implementation complete in PR #1740; awaiting merge
Scope: bug fixes and internal refactors only — no new public API surface, no behavior changes beyond fixing verified bugs.
Working model: one coherent D2 graph implementation, backed by direct oracle assertions and narrow boundary adapters.
Progress:
materialize()subquery resolves to[]when its correlation predicate targets a joined alias #1704), initially-null correlation plus transition controls (materialize(q…findOne())resolves tonull, notundefined, when the correlation key is itself null #1706), and duplicate-key join multiplicity (innerJoin drops a live-query result when one of many children is deleted #1703).where/orderBy/selectQ2 shapes, including synced and optimistic publication, recovery, rollback, and replacement histories.The detailed issue evidence below records the pre-implementation state. Section 3 and live GitHub issue status are authoritative for the completed implementation.
1. What's happening
The includes system (subquery-in-select,
toArray(),materialize()) has produced a steadystream of correctness bugs: silently misrouted data, dropped children, stale sort order, broken
adapter reactivity, and permanent loading states. Every claim below was verified against current
mainwith a red/green test (tests live on this branch, indescribe('cluster-verification …')blocks appended to existing test files).
issuesinclude is fully replaced by tag rows, real issues lost, nested comments emptypackages/db/tests/query/includes.test.ts(cluster-verification, claim A)orderByin an include ignored after optimistic update on the child collectionmain: the exact optimistic child-reorder regression is green and retained in the recompute oracle; #1444 closed and PR #1496 closed unmerged as supersededpackages/db/tests/query/includes-oracle.property.test.ts(regression seed: optimistic child reorder matches recomputation)allCollectionsReady()never true because per-row lazyloadSubsetnever firespackages/db/tests/query/includes-lazy-loading.test.ts(#1510 block)toArraychildren skip the fast-path snapshotincludeInitialState: false⇒ the onlyrequestSnapshotfires per parent row, after the progressive buffering window closedtoArrayinclude updates never reach the rendereddatastoredatais stale; thestatemap and underlying collection row do updatepackages/solid-db/tests/useLiveQuery.test.tsx(#1571 block)nulland never becomes reactiveCollectionfrom first render and populates on insert (caveat:Collectioninstances aren't Solid-reactive by design)has()reclassification +config.utilsguard)includes.test.ts, claim C (green)toArraydrops children when correlation keys overlap across parent groupsincludes.test.ts, claim D (green control)subscribers:changecompensate; likely fixed since the reported versionpackages/query-db-collection/tests/query.test.ts(#1488 block, green)New reports since this RFC was first drafted:
This is a P2 multiplicity failure outside the include wrapper: several differential rows can map
to one public key, so the reducer must retain net multiplicity and publish a delete only when the
count crosses zero. Add a direct join driver and reduced expected-failure trace before PR 2.
materialize()subquery resolves to[]when its correlation predicate targets a joined alias #1704 / PR fix: correlated subquery resolves empty when correlation targets a joined alias #1705 — a correlated materialized subquery is empty when its predicate targets ajoined alias. This is direct evidence for P1's scoped source identity: the compiler assumes the
correlation ref belongs to the subquery's main
fromalias. Keep fix: correlated subquery resolves empty when correlation targets a joined alias #1705's initial and incrementaltests; it may land as a narrow fix, while PR 2 removes the alias assumption structurally.
materialize(q…findOne())resolves tonull, notundefined, when the correlation key is itself null #1706 / PR fix: resolve materialize(findOne()) to undefined when the correlation key is null #1707 —materialize(findOne())leaks the internalnullplaceholder when thecorrelation key is null. The typed result is
undefined. Treat fix: resolve materialize(findOne()) to undefined when the correlation key is null #1707 as an independent narrowsemantic fix. The oracle narrows the current failure to rows first observed with a null key; matched→null and unmatched non-null cases are green controls.
materialize()fields in live queries #1713 — a parent-only update empties materialized fields in a live query layered over another live query. Open PR test(db): cover layered includes publication #1736 turns this into a full publication matrix: two sibling materializations; Q1 with and without a join; Q2 pass-through,where,orderBy, andselect; synced and optimistic parent publication; child-triggered recovery; rollback; and same-route and route-changing replacements. Q1 must remain exact while the known failure classifier permits only both Q2 materializations becomingnull. Keep Updating a parent source row emptiesmaterialize()fields in live queries #1713 open until the runtime fix turns these gates green.materializeis dropped when the middle level is shared by more than one parent row #1685 / PR Fix #1685 #1686 — three nestedmaterialize(findOne())levels drop the deepest row for oneroot when distinct middle rows point to the same shared row. RED — confirmed: the first
recursive consumer deletes the shared row's
INCLUDES_ROUTINGstamp, so the second cannotregister the next routing edge. This is another P3 shared-routing failure. PR Fix #1685 #1686 defers stamp
cleanup until the outermost flush; land it as a narrow stopgap with the exact scalar regression,
then delete that lifetime bookkeeping in PR 3.
row from
sharedId: 1to a missingsharedId: 2updates the visible foreign key but retains theold shared row in the existing incremental query. A fresh query over the same collections matches
recomputation, isolating the fault to incremental nested routing/materialization. The reduced
five-step expected-failure seed now lives in the scalar-materialization driver. Treat this as
another P3 routing-lifetime failure, not an isolated symptom fix.
concat(toArray(...))can both retain old membership instead of applying the whole hand-off. These are two projections of one batch-routing defect, covered by one reduced scenario under both materializations.undefinedon the next render after a parentcollection.update()(Electric + React), self-healing on forced re-render. Updating a parent source row emptiesmaterialize()fields in live queries #1713 reduces this toan adapter-free layered live query and shows the bad placeholder persists until a child changes.
Publication-contract class: this strengthens the case for PR 4 below over per-adapter shims.
useLiveQuerywith multi-level includes costs up to ~100ms per run. Open PR test(db): add includes work and shape oracles #1738 adds a clock-free semantic gate for the report's 20×2×5×10 tree. All 20/40/200/2,000 source rows must be delivered byroots.preload(), traversing the public result must cause no further source delivery, and the reachable result must expose 20/40/200 child collections containing 40/200/2,000 rows. The public API cannot count internal allocations or discarded intermediate collections, so PR 3 still needs a benchmark and production-boundary instrumentation for construction-cost claims.query-db-collection. Open PR test(query-db): cover ownership lifecycles #1737 confirms that on-demand store entries are removed while queries still use them when reusing an existing observer #1488's reported observer-reuse path no longer reproduces on currentmain: observer retirement and ownership cleanup are atomic, and cached reacquisition re-registers ownership. It also proves that Ref count misassumption #1631 and PR Fix/live query record drop on subset unmount #1656's persisted-owner failure still reproduce at exact checkpoints through production transactional metadata. Keep Ref count misassumption #1631 as the live red gate; Fix/live query record drop on subset unmount #1656 remains closed unmerged.Reviewed but owned elsewhere: #1662 is already evidence E13 in loadSubset RFC #1657; #1698 is
an Electric/sync ingestion defect under RFC #1659; #1708 is a general join-index selection issue;
#1712 asks for a new remote-join feature; and #1721 changes React hook state ownership under the
live-query platform work. They are not gates for this includes RFC.
Status audit (2026-08-17): #1716–#1739 are merged. PR #1740 contains the production graph rewrite and the final oracle expansion. Its directly owned gates are green; related adapter, ownership, and performance reports remain scoped as stated in the PR body.
Why these keep happening
The bugs are not independent. The includes system compiles each include into its own child D2
pipeline (sound), but then reconstructs include semantics in a ~2,300-line imperative output layer
(
packages/db/src/query/live/collection-config-builder.ts) using alias maps, child collectionregistries, correlation routing indexes, pending-change buffers, and in-place parent-row mutation.
Correctness rests on identities that are only implicit:
the compiler flattens all includes aliases into one namespace, so
{ i: issues }and{ i: tags }share one D2 input (Duplicate alias in sibling includes silently breaks nested children #1454).correlated child set, and a shared result row's routing metadata must outlive every recursive
consumer (3-level nested
materializeis dropped when the middle level is shared by more than one parent row #1685). A destructively-drained shared buffer can't represent that fan-out(3-level nested toArray: shared buffer in createPerEntryIncludesStates drops children when correlation keys overlap across parent groups #1501/fix(db): propagate changes through nested toArray includes at depth 3+ #1457 — patched by fix(db): nested toArray includes drop children when sibling groups share a correlation key (#1501) #1607, but the shared-state design remains).
one public key (innerJoin drops a live-query result when one of many children is deleted #1703), so deleting one contributor must not delete the result while another
remains. A
(-1,+1)pair must become one atomic replacement including its order metadata. Today "insert vs update" is decided per call site —three near-copies of the accumulator exist (parent/child/nested), and the child copy retained a
stale
orderByIndex(TanStack DB "includes" ignores orderBy after optimistic update #1444). The landed fix: reconcile duplicate live query child inserts #1600 fix decides by checkingcollection.has(key)mid-flush, which works but keeps classification dependent on whatever state exists at flush time.
flushIncludesStatemutates parent rows in placeand force-emits through
changesManager.emitEvents(events, true)to defeat the collection's owndeepEqualssuppression. React's version-bump mostly tolerates this; Solid'sreconciledoesnot (Include value updates break with solidjs #1571), Nested includes are undefined on next render after collection.update #1635 suggests React has its own window, and each future adapter needs its own
workaround.
involved collections; lazy children that were never demanded (fix(db): live query stuck loading when subquery-in-select inner is cold on-demand #1510) or progressive children
whose fast-path window is timing-dependent (Progressive sync: nested toArray subqueries skip the fast-path snapshot #1533) fall through it.
2. Revised design direction
The completed oracle suite keeps the five original principles but changes their boundaries:
The target data path is:
CorrelatedRelationis therefore a protocol implemented by several small stateful components, not one object that owns contents, subscribers, demand, and publication. Rebuilding all of those responsibilities inside one operator would recreateCollectionConfigBuilderunder a new name.Seven architecture axes
JSON.stringifyof mutable context.Stable defect catalog
The expected-failure tests use exact classifiers and checkpoints. The RFC names each class so it remains visible outside test code:
The optimistic suite does not define a separate architecture axis: its only relationship failure is the existing deep-rekey class reached through another producer. Optimistic changes remain weighted inputs to the same ledger and route protocol.
The batch suite also argues against replacing the scheduler or D2 graph-run boundary. Atomic delivery, reverse order, same-ID replacement, and fresh-route mirrors are green. The narrow split/new-ID/handed-off-route defect belongs in route ownership and staged application.
Normative laws
These are permanent contracts even if component names change:
3. Implementation outcome
PR #1740 implements the architecture as one coherent change rather than the earlier staged cutover. The landed oracle work made that safe: every known defect has an exact checkpoint or structural classifier, and the production branch removes those classifiers instead of weakening them.
The runtime now uses opaque compiled source identity and D2 weighted relations for contribution, route, ordering, and nested propagation.
BucketFacadeAdapterowns only public Collection identity and subscription.SubsetDemandControllerowns only asynchronous demand coverage, cancellation, and readiness. Coherent publication installs child facades and root rows before observers run. The legacy alias maps, route registries, reverse indexes, recursive internal Collections, depth buffers, and imperative snapshot drains are removed.The final testing strategy combines deterministic regression traces, 172 exhaustive micro-domain cases, random route-changing FastCheck histories, Collection/array/materialized comparison, scheduled demand interleavings, and cross-formulation nested/flat/per-parent/TLP equivalence.
TANSTACK_DB_ORACLE_SEEDreplays random campaigns,TANSTACK_DB_ORACLE_RUNS_MULTIPLIERscales them, andTANSTACK_DB_ORACLE_STATISTICS=1reports generated coverage. The default eight-file gate passes 219/219. A 100x run completed all assertions without a semantic counterexample; Vitest emitted one post-run worker-RPC timeout, after which the normal gate exited cleanly.Generic DBSP laws are deliberately separate because they belong to
@tanstack/db-ivm, not the includes facade. Follow-up #1741 tracksQ(x + Δ) = Q(x) + Q^Δ(x, Δ)coverage for core differential operators.4. Non-goals / rejected approaches
flush sub-passes, no per-adapter cloning beyond the temporary A2 shim, no growing the
readiness-exclusion list beyond A1's stopgap. Each of these closes one issue while making the
state machine harder to reason about — PRs 2–5 exist to delete them.
5. Risks
LoadSubsetOptions.signalis optional for compatibility; adapters that fetch asynchronously should honor it before installing rows.Appendix: relationship to reviewed PRs
mainpasses the retained exact regression and #1444 is closedIssue #1488's reported observer-reuse path does not reproduce on current
main, as pinned by #1737. The same oracle reproduces #1631 and PR #1656 through different ownership-loss mechanisms, so the ownership/refcount lifecycle remains a required gate rather than being dismissed with #1488's green result.Issue #1505 is closed; its underlying concern (include fields transiently unmaterialized, types
don't admit it) is addressed by PR 4's always-attached include values.