From 6602d789512fc32dcf3b8930078895f35877132f Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 1 Oct 2026 10:12:28 -0600 Subject: [PATCH 1/2] fix(db): keep source identity across query scopes Live queries returned no rows when an include reused an alias from a joined from() subquery (#1975). The optimizer rebuilt CollectionRefs with fresh SourceIds; since #1877 compiles the optimized subquery, the compiler missed those IDs and fell back to alias text, which a sibling scope had claimed. - Optimizer copies, wraps, and collapses reuse the CollectionRef. - Compilation binds inputs by SourceId only, so a lost identity raises CollectionInputNotFoundError instead of reading another source. - No scope inside an include can shadow an ancestor alias, including in unionAll() branches and from()/join subqueries. - One query cannot give two sources the same alias. Adds a generated alpha-renaming oracle across sibling scopes with an independent recomputation model, plus the review record and contract updates. Co-authored-by: Isaac --- .../keep-source-identity-across-scopes.md | 5 + docs/contributing/oracle-coverage.md | 282 +++---- .../issue-1975-scope-identity.md | 125 +++ packages/db/src/query/compiler/index.ts | 58 +- packages/db/src/query/compiler/joins.ts | 2 +- packages/db/src/query/live/ARCHITECTURE.md | 51 +- packages/db/src/query/optimizer.ts | 22 +- packages/db/tests/oracle-config.ts | 1 + packages/db/tests/oracle-replay-manifest.ts | 2 +- .../query/includes-oracle.property.test.ts | 409 +++++++++- .../query/includes-scope-identity-oracle.ts | 772 ++++++++++++++++++ .../db/tests/query/validate-aliases.test.ts | 227 ++++- 12 files changed, 1776 insertions(+), 180 deletions(-) create mode 100644 .changeset/keep-source-identity-across-scopes.md create mode 100644 docs/contributing/oracle-reviews/issue-1975-scope-identity.md create mode 100644 packages/db/tests/query/includes-scope-identity-oracle.ts diff --git a/.changeset/keep-source-identity-across-scopes.md b/.changeset/keep-source-identity-across-scopes.md new file mode 100644 index 0000000000..ce15ecfb9d --- /dev/null +++ b/.changeset/keep-source-identity-across-scopes.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Fix live queries that returned no rows when an include reused an alias from a joined `from()` subquery. Query optimization now keeps each source's identity, and compilation reads source inputs by identity instead of by alias. An include that reuses a parent subquery alias is now rejected with `DuplicateAliasInSubqueryError` instead of returning wrong rows. Two joins that share an alias in one query are also rejected. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index d66b3d3d7e..b3055f8fd3 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -49,34 +49,35 @@ Model-only terms such as an appointment ledger or fault tape remain local and say what production facts they abstract. This is a semantic review, not a rule that test identifiers must copy production's private data structures. -| Surface | Status | Completed or next owner | -| --- | --- | --- | -| Ordered relations and BTree | Complete | The signed top-K relation, BTree/Map refinement model, and DBSP incrementalization laws are literate. | -| D2 operator work | `packages/db-ivm/tests/d2-work-oracle.test.ts` | A finalized 65-branch graph receives one direct input at the first, middle, or last branch, then a second input on a different branch. The oracle compares exact forwarded messages and operator invocations after each `run()`: branches without queued input perform no operator work. A connected reversed-registration pair must drain in the same run, and an empty unfinalized graph rejects. The old unconditional loop fails with 64 idle calls per turn. This bounded work check does not establish elapsed latency, fused operators, arbitrary graph topologies, or provider-level live-query work. | -| Includes and publication | Complete | The central recomputation model plus cross-formulation, temporal demand, layered publication, Collection facade lifecycle and space bounds, route-context transport, functional projection, query-shape, optimistic, and source-work owners are literate. They keep the architecture document as their contract source. | -| Collection lifecycle | Complete | The shared logical-owner/acquisition-attempt/sync-run grammar plus mutation admission, lifecycle trace, publication, replay, disposal, and transaction-refinement boundaries are literate. | -| Paced mutations | Partial | The virtual-clock owner checks queue order, bounded admission, cleanup draining in FIFO/LIFO order at the configured wait, admitted writes after separate Collection cleanup, post-cleanup queue rejection with a disposal reason, pending debounce drain at the last call's quiet edge, pending trailing throttle drain at its regular edge, debounce and throttle edge options including omitted options, immediate rejection of dropped leading-only calls and both-disabled calls, caller-option immutability, and settlement. New debounce/throttle admission after cleanup, failed persistence, and broader generated schedules remain open. | -| Optimistic state | Complete | The independent base/intent/source-queue graph plus outcome, transaction-payload, and publication drivers are literate. | -| Drafts and native values | Complete | Native differential behavior, draft change tracking, detachment and its class-instance exception, hostile keys, aliases, cycles, and Map/Set live iteration are literate. | -| Query DB and observer | Complete | Query-scope row ownership, subset identity and cancellation, failure and recovery, and the per-listener eligibility ledger are literate. | -| Query DB SSR dehydration | Fixed QueryClient/Seroval boundary | Successful on-demand entries with a compound predicate, a request signal, and an ordered cursor with a custom comparator retain their row and user metadata through Seroval's reconstructed stream payload and Query Core hydration; request options are absent from the stream, while query functions retain the original values. The TanStack Start router and browser Collection resume path still need a host witness. | -| Ordered acquisition | Complete | Exact demand identity, applied settlement, independent pagination recomputation, request work, lifecycle products, replay authority, source-generation readiness, and transaction-refinement abort boundaries are literate. | -| Indexed predicate filtering | Complete for bounded dotted-path collision | An independent row filter checks direct predicates and public callback predicates for a dotted scalar field beside a nested path, with and without an index. Selected-field ordering checks the same distinction through `$selected`. | -| WHERE predicate publication | Complete for bounded predicate and sync-transaction grammar | The contract, Kleene reference evaluator, snapshot and change-history grammar, three subscriber consumers plus direct snapshot, and per-commit key-set refinement check are literate. A fixed same-key update checks live-row and direct-subscriber payloads; a focused descriptor boundary checks stored-row prefilter safety. Generated cleanup and restart histories for filtered subscribers remain open. | -| Joined result keys | Complete for bounded two-source key grammar | The contract, nested-loop pair model, delimiter-, number-like, infinite, and `NaN` key grammar, public join driver, and per-checkpoint pair and key-count check are literate. Joins over subqueries, more than two sources, custom `getKey`, and optimistic mutations remain outside this owner. | -| D2 Index storage | Complete for bounded prefix grammar | The contract, plain-`Map` multiset model, prefixed and unprefixed value grammar, `Index` driver, and per-addition `get`/`has` check are literate. Compaction, presence tracking, and structural payloads remain outside this owner. | -| Lazy target path identity | Focused compiler boundary | A same-source union/coalesce witness keeps dotted and nested demand paths distinct during target deduplication. | -| Correlated include path identity | Focused public route-context witnesses | One-level and nested includes keep dotted and nested parent paths, including ancestor aliases, distinct. Conditional result paths receive separate routes. Fixed fixtures cover initial reads and selected source updates; other recursive source forms and arbitrary path segments remain outside this witness. | -| Join equality and cold acquisition | Complete | Independent cold relational recomputation, acquisition evidence, established equality domains, replacements, and scan/index routes are literate. | -| Opaque backend pagination | Complete | The full-relation value model plus opaque token, cache generation, publication, browser acquisition, and live-window integration owners are literate. | -| Electric and TrailBase | Complete | Electric replica and recovery models, installed-SDK HTTP delivery, PostgreSQL serialization, and TrailBase's controlled RecordApi/native-stream lifecycle are literate with their real-provider limits intact. | -| PowerSync | Complete | Patch conservation, effective-update receipts, metadata and falsey changes, declared-view keys, transformed schema output, logging, cleanup, and native SQLite reach are literate. | -| SQLite persistence and native hosts | Complete | Persisted hydration/replay and ownership, shared-handle driver transaction laws, OPFS page and diagnostic state machines, and the 113-law native conformance manifest are literate. Native execution remains distinct from registration and shim evidence. | -| Offline execution | Complete | FIFO retry, scheduler eligibility, leadership replay, transaction settlement, and typed wire serialization are literate. | -| Frameworks | Complete | Shared live-query and infinite-query models are literate. Each framework keeps its own realm, ownership, and scheduling driver. | -| Structural values and ordered primitives | Complete | Structural hashing, deep equality, comparison, cursor denotation, index refinement, and query-identity output equivalence are literate. | -| Boundary refinements | Complete | Cleanup/restart admission, metadata publication, retained state, acquisition cells, D2 source reconciliation, top-K support windows, and nested Query work bounds are literate. | -| Small structures and test mechanics | Complete | SortedMap, cleanup appointments, and guarded replay are literate. | +| Surface | Status | Completed or next owner | +| ---------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ordered relations and BTree | Complete | The signed top-K relation, BTree/Map refinement model, and DBSP incrementalization laws are literate. | +| D2 operator work | `packages/db-ivm/tests/d2-work-oracle.test.ts` | A finalized 65-branch graph receives one direct input at the first, middle, or last branch, then a second input on a different branch. The oracle compares exact forwarded messages and operator invocations after each `run()`: branches without queued input perform no operator work. A connected reversed-registration pair must drain in the same run, and an empty unfinalized graph rejects. The old unconditional loop fails with 64 idle calls per turn. This bounded work check does not establish elapsed latency, fused operators, arbitrary graph topologies, or provider-level live-query work. | +| Includes and publication | Complete | The central recomputation model plus cross-formulation, temporal demand, layered publication, Collection facade lifecycle and space bounds, route-context transport, functional projection, query-shape, optimistic, and source-work owners are literate. They keep the architecture document as their contract source. | +| Collection lifecycle | Complete | The shared logical-owner/acquisition-attempt/sync-run grammar plus mutation admission, lifecycle trace, publication, replay, disposal, and transaction-refinement boundaries are literate. | +| Paced mutations | Partial | The virtual-clock owner checks queue order, bounded admission, cleanup draining in FIFO/LIFO order at the configured wait, admitted writes after separate Collection cleanup, post-cleanup queue rejection with a disposal reason, pending debounce drain at the last call's quiet edge, pending trailing throttle drain at its regular edge, debounce and throttle edge options including omitted options, immediate rejection of dropped leading-only calls and both-disabled calls, caller-option immutability, and settlement. New debounce/throttle admission after cleanup, failed persistence, and broader generated schedules remain open. | +| Optimistic state | Complete | The independent base/intent/source-queue graph plus outcome, transaction-payload, and publication drivers are literate. | +| Drafts and native values | Complete | Native differential behavior, draft change tracking, detachment and its class-instance exception, hostile keys, aliases, cycles, and Map/Set live iteration are literate. | +| Query DB and observer | Complete | Query-scope row ownership, subset identity and cancellation, failure and recovery, and the per-listener eligibility ledger are literate. | +| Query DB SSR dehydration | Fixed QueryClient/Seroval boundary | Successful on-demand entries with a compound predicate, a request signal, and an ordered cursor with a custom comparator retain their row and user metadata through Seroval's reconstructed stream payload and Query Core hydration; request options are absent from the stream, while query functions retain the original values. The TanStack Start router and browser Collection resume path still need a host witness. | +| Ordered acquisition | Complete | Exact demand identity, applied settlement, independent pagination recomputation, request work, lifecycle products, replay authority, source-generation readiness, and transaction-refinement abort boundaries are literate. | +| Indexed predicate filtering | Complete for bounded dotted-path collision | An independent row filter checks direct predicates and public callback predicates for a dotted scalar field beside a nested path, with and without an index. Selected-field ordering checks the same distinction through `$selected`. | +| WHERE predicate publication | Complete for bounded predicate and sync-transaction grammar | The contract, Kleene reference evaluator, snapshot and change-history grammar, three subscriber consumers plus direct snapshot, and per-commit key-set refinement check are literate. A fixed same-key update checks live-row and direct-subscriber payloads; a focused descriptor boundary checks stored-row prefilter safety. Generated cleanup and restart histories for filtered subscribers remain open. | +| Joined result keys | Complete for bounded two-source key grammar | The contract, nested-loop pair model, delimiter-, number-like, infinite, and `NaN` key grammar, public join driver, and per-checkpoint pair and key-count check are literate. Joins over subqueries, more than two sources, custom `getKey`, and optimistic mutations remain outside this owner. | +| D2 Index storage | Complete for bounded prefix grammar | The contract, plain-`Map` multiset model, prefixed and unprefixed value grammar, `Index` driver, and per-addition `get`/`has` check are literate. Compaction, presence tracking, and structural payloads remain outside this owner. | +| Lazy target path identity | Focused compiler boundary | A same-source union/coalesce witness keeps dotted and nested demand paths distinct during target deduplication. | +| Correlated include path identity | Focused public route-context witnesses | One-level and nested includes keep dotted and nested parent paths, including ancestor aliases, distinct. Conditional result paths receive separate routes. Fixed fixtures cover initial reads and selected source updates; other recursive source forms and arbitrary path segments remain outside this witness. | +| Alias scope identity | Generated cross-scope alpha-renaming with an independent model | Optimizer copies, wraps, and collapses keep `SourceId`; compilation binds inputs by `SourceId` only; includes cannot shadow a parent subquery alias. Unreached forms and channels are listed in the owner row. | +| Join equality and cold acquisition | Complete | Independent cold relational recomputation, acquisition evidence, established equality domains, replacements, and scan/index routes are literate. | +| Opaque backend pagination | Complete | The full-relation value model plus opaque token, cache generation, publication, browser acquisition, and live-window integration owners are literate. | +| Electric and TrailBase | Complete | Electric replica and recovery models, installed-SDK HTTP delivery, PostgreSQL serialization, and TrailBase's controlled RecordApi/native-stream lifecycle are literate with their real-provider limits intact. | +| PowerSync | Complete | Patch conservation, effective-update receipts, metadata and falsey changes, declared-view keys, transformed schema output, logging, cleanup, and native SQLite reach are literate. | +| SQLite persistence and native hosts | Complete | Persisted hydration/replay and ownership, shared-handle driver transaction laws, OPFS page and diagnostic state machines, and the 113-law native conformance manifest are literate. Native execution remains distinct from registration and shim evidence. | +| Offline execution | Complete | FIFO retry, scheduler eligibility, leadership replay, transaction settlement, and typed wire serialization are literate. | +| Frameworks | Complete | Shared live-query and infinite-query models are literate. Each framework keeps its own realm, ownership, and scheduling driver. | +| Structural values and ordered primitives | Complete | Structural hashing, deep equality, comparison, cursor denotation, index refinement, and query-identity output equivalence are literate. | +| Boundary refinements | Complete | Cleanup/restart admission, metadata publication, retained state, acquisition cells, D2 source reconciliation, top-K support windows, and nested Query work bounds are literate. | +| Small structures and test mechanics | Complete | SortedMap, cleanup appointments, and guarded replay are literate. | ### Guide-audit boundary handoffs @@ -211,15 +212,15 @@ This inventory records the permanent authority for the September 17 fix wave. It distinguishes an executable oracle from a specialized real-provider authority and does not award oracle credit for a filename alone. -| PR | Classification | Permanent authority and campaign | -| --- | --- | --- | -| [#1831](https://github.com/TanStack/db/pull/1831) | Explicit oracle | PowerSync's `packages/powersync-db-collection/tests/correctness-oracle.test.ts` crosses real PowerSync receipts and native SQLite behavior. It runs in the package test campaign and the focused `test:oracles` campaign; portable declarations retain compiler authority. | -| [#1832](https://github.com/TanStack/db/pull/1832) | Equivalent specialized authority | `packages/electric-db-collection/e2e/sql-predicate-semantics.e2e.test.ts` and `packages/electric-db-collection/e2e/subset-sql-acceptance.e2e.test.ts` run through the package's real-provider `test:e2e` campaign. Compiler unit tests are collateral, not substitutes for either service boundary. | -| [#1833](https://github.com/TanStack/db/pull/1833) | Explicit oracle | `packages/db/tests/query/pagination-oracle.property.test.ts` owns inherited collection collation, actual `item2`/`item10` order, exact request options, hostile lexical/numeric controls, and both scan and auto-index paths. It runs in `@tanstack/db`'s `test:oracles` campaign. | -| [#1834](https://github.com/TanStack/db/pull/1834) | Explicit oracle | `packages/db/tests/query/cold-join-reconciliation-oracle.test.ts` owns join/predicate equality equivalence across the established value domains, binary/string and nullish controls, replacement histories, raw lazy demand, and scan/auto-index paths. It runs in `@tanstack/db`'s `test:oracles` campaign. | -| [#1835](https://github.com/TanStack/db/pull/1835) | Explicit oracle | The existing `packages/db/tests/collection-state-retention-oracle.property.test.ts` and `packages/db/tests/optimistic-transaction-oracle.property.test.ts` owners cover separate collection-state and transaction-history laws. Both were already registered in `@tanstack/db`'s `test:oracles` campaign; focused storage/local-only tests remain collateral. | -| [#1837](https://github.com/TanStack/db/pull/1837) | Explicit oracle | The offline scheduler, leadership replay, and serializer owners cover selective replay retirement, durable per-ID settlement, stale-read fencing, lifecycle recovery, native scalar encoding, and prior wire compatibility. They run in the package test campaign; generated owners expose `OFFLINE_ORACLE_{SEED,PATH,RUNS}` or the scheduler's `TANSTACK_DB_OFFLINE_ORACLE_*` replay interface. | -| [#1842](https://github.com/TanStack/db/pull/1842) | No shipped-law case | The PR changed only focused observer tests and introduced no production behavior. `packages/db/tests/live-query-observer.test.ts` remains the correct evidence; no synthetic oracle or campaign claim is added. | +| PR | Classification | Permanent authority and campaign | +| ------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [#1831](https://github.com/TanStack/db/pull/1831) | Explicit oracle | PowerSync's `packages/powersync-db-collection/tests/correctness-oracle.test.ts` crosses real PowerSync receipts and native SQLite behavior. It runs in the package test campaign and the focused `test:oracles` campaign; portable declarations retain compiler authority. | +| [#1832](https://github.com/TanStack/db/pull/1832) | Equivalent specialized authority | `packages/electric-db-collection/e2e/sql-predicate-semantics.e2e.test.ts` and `packages/electric-db-collection/e2e/subset-sql-acceptance.e2e.test.ts` run through the package's real-provider `test:e2e` campaign. Compiler unit tests are collateral, not substitutes for either service boundary. | +| [#1833](https://github.com/TanStack/db/pull/1833) | Explicit oracle | `packages/db/tests/query/pagination-oracle.property.test.ts` owns inherited collection collation, actual `item2`/`item10` order, exact request options, hostile lexical/numeric controls, and both scan and auto-index paths. It runs in `@tanstack/db`'s `test:oracles` campaign. | +| [#1834](https://github.com/TanStack/db/pull/1834) | Explicit oracle | `packages/db/tests/query/cold-join-reconciliation-oracle.test.ts` owns join/predicate equality equivalence across the established value domains, binary/string and nullish controls, replacement histories, raw lazy demand, and scan/auto-index paths. It runs in `@tanstack/db`'s `test:oracles` campaign. | +| [#1835](https://github.com/TanStack/db/pull/1835) | Explicit oracle | The existing `packages/db/tests/collection-state-retention-oracle.property.test.ts` and `packages/db/tests/optimistic-transaction-oracle.property.test.ts` owners cover separate collection-state and transaction-history laws. Both were already registered in `@tanstack/db`'s `test:oracles` campaign; focused storage/local-only tests remain collateral. | +| [#1837](https://github.com/TanStack/db/pull/1837) | Explicit oracle | The offline scheduler, leadership replay, and serializer owners cover selective replay retirement, durable per-ID settlement, stale-read fencing, lifecycle recovery, native scalar encoding, and prior wire compatibility. They run in the package test campaign; generated owners expose `OFFLINE_ORACLE_{SEED,PATH,RUNS}` or the scheduler's `TANSTACK_DB_OFFLINE_ORACLE_*` replay interface. | +| [#1842](https://github.com/TanStack/db/pull/1842) | No shipped-law case | The PR changed only focused observer tests and introduced no production behavior. `packages/db/tests/live-query-observer.test.ts` remains the correct evidence; no synthetic oracle or campaign claim is added. | [PR #1816](https://github.com/TanStack/db/pull/1816) preserves existing witnesses, repairs false-green assertions and drivers, adds missing histories, and includes @@ -234,39 +235,40 @@ separate ledger, not another 13 unique defects. Paths below are relative to the repository root. Follow each suite's domain comment and the current API/architecture contract before extending its model. -| Surface | Primary executable owners | Independent judgment and important limit | -| --- | --- | --- | -| Ordered relations and BTree | [top-K relation oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/topk-relation-oracle.test.ts), [fractional-index window moves](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/topKWithFractionalIndex.test.ts), [BTree/Map](https://github.com/TanStack/db/blob/main/packages/db/tests/btree-map-oracle.test.ts), [incrementalization laws](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/incrementalization-law.property.test.ts) | Independent ordered relations and cumulative signed output. Top-K consolidation compares same-key values without hashing, including cyclic replacements and fresh transient cancellation. The fractional-index test checks an empty output message for a window move after all rows retract; it does not prove downstream lazy acquisition. Other hash-based operators retain hashing's declared domain. Algebra does not specify client readiness. The BTree/Map oracle checks point operations, neighbor pairs, and full, partial, boundary, exclusive-high, and empty range scans for integer keys and node sizes 4–8. It does not check balance or space, and it assumes consistent comparators ([code-weight BTree review](oracle-reviews/code-weight-btree-trim.md)). | -| Includes and publication | [cross-formulation](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts), [temporal](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-temporal-oracle.test.ts), [Collection includes](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-collection-oracle.property.test.ts), [architecture and complete suite map](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/ARCHITECTURE.md#executable-contracts) | Per-parent/flat-join/partition relations, callback-time rows, nested values, and route histories. Fragmented lazy-demand consolidation covers delta growth and successful, rejected/retried, and obsolete replacements at the Collection request boundary; it observes exact key unions and abort checkpoints. A compiled-includes fixture makes adapter unload evict owned child rows and proves a rejected union cannot remove established public child rows. Retry after that terminal live-query failure remains covered at the acquisition boundary; no automatic in-place recovery is claimed. Observe raw promised order; fresh queries do not establish continuous publication safety. | -| Collection lifecycle | [mutation startup](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-mutation-startup-oracle.test.ts), [change-event history](https://github.com/TanStack/db/blob/main/packages/db/tests/change-event-history-oracle.test.ts), [history](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-history.property.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-publication.property.test.ts), [replay](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-replay-oracle.property.test.ts), [effect disposal](https://github.com/TanStack/db/blob/main/packages/db/tests/effect-disposal-oracle.test.ts), [PR #1902 review](oracle-reviews/pr-1902-change-event-history.md), [batch-order decision review](oracle-reviews/change-event-batch-order.md) | Core Collection `insert`/`update`/`delete` admission while `startSync:false` is idle; complete public-row/change-message agreement across bounded one- and two-key histories plus replayable four-key campaigns; a two-key deferred sync history checks each key's causal insert/update/delete trace while allowing any cross-key interleaving. Reversing all deferred messages fails that check; reversing each sync transaction's distinct-key messages passes. Queued duplicate admission and cancellation, ownership and phase histories, exact caller/error/publication evidence, and late completion and restart have separate witnesses. Generated deferred histories with cancellation or reentrant callbacks need a witness in the change-event history owner before claiming general batch-shape coverage. Query write utilities and effect self-dependent disposal remain separate contracts. | -| Paced mutations | [virtual-clock oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/paced-mutations-oracle.test.ts) | `createPacedMutations` with queue front/back options, bounded waiting capacity at zero and one, cleanup draining in FIFO/LIFO order after a held write, cleanup timing at the configured wait for immediate writes, admitted writes after separate Collection cleanup, debounce and throttle schedules with explicit and omitted options, and first-leading throttle execution at epoch zero. Finite histories compare optimistic rows, returned transaction identity and receipt outcome, execution order and virtual-clock time. Held writes verify queue serialization. Leading-only throttle and debounce witnesses reject skipped calls immediately with their named dropped-call errors, including omitted edges, both edges disabled, and same-row rollback while a prior write is held. Capacity witnesses cover overflow rejection, distinct-key optimistic rollback, same-key admitted-write survival, and in-flight versus waiting admission. Cleanup witnesses check repeated queue cleanup, eventual admitted receipt settlement, direct queue strategy rejection of new callbacks, public post-cleanup rollback with `QueueDisposedError`, pending debounce transactions that wait until the last call's quiet edge after separate Collection cleanup, and a trailing throttle timer that runs at its regular edge after separate Collection cleanup. Deferred custom queue and batch callbacks check compatibility with `void` and `false` execute results, respectively. Frozen options verify factory non-mutation; a mutable option witness checks that debounce uses its construction-time trailing setting. New debounce/throttle admission after cleanup, failed persistence, broader capacities and schedules remain outside this owner. | -| Optimistic state | [history model](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-oracle.ts), [generated histories](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-transaction-oracle.property.test.ts), [truncate capture ownership](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-truncate-ownership-oracle.property.test.ts), [outcomes](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-outcomes.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-publication.test.ts) | Independent whole-row snapshots, rollback dependencies, captured truncate ownership, metadata and prior-value events. Never rebase a pending snapshot merely to simplify the model. | -| Drafts and native values | [proxy](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy.test.ts), [detachment](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-detachment-contract.test.ts), [iteration](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-iteration-contract.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. Set and Map reorder histories check draft patches and stored iteration order after replacement and clear-and-readd; RegExp replacement/reversion histories check `lastIndex`. General native-mutator and symbol-write support is not established by a plain-object oracle. | -| Query DB and observer | [ownership](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts), [load lifecycle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts), [observer histories](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-observer-history.property.test.ts) | Real QueryClient boundary, applied-settlement barriers for existing and cached demand, mutation-handler Collection identity, terminal fetch-record lifecycle, and a per-listener eligibility ledger, not a duplicate dispatch queue. The bounded handler grammar crosses insert, update, delete, parameter and mutation-alias access, refetch, and clearError. A held clearError retry retains the prior public error through initial, background, and Collection application failures; success clears it, failure advances the count, including a repeated error at clock time zero. An intermediate Query retry attempt does not recount the previous background error; a held final attempt can succeed or fail. A held application after successful Query fetch keeps the error visible until the applied result. A mocked persisted-baseline scan holds an older success while a newer terminal Query failure arrives; the newer public error and count survive application, including when the Error object and clock tick repeat. Native SQLite persistence, concurrent retries across multiple tracked Queries, mutation-handler fetch-boundary settlement, and deferred result application remain outside this witness. The mutation overlap grammar crosses both start orders. Check reentry, peer survival, FIFO and disposal independently of final rows. | -| Query DB SSR dehydration | `packages/query-db-collection/tests/ssr-dehydration-oracle.test.ts`, [review record](oracle-reviews/issue-1950-ssr-dehydration.md) | A plain Query cache control, a live on-demand compound predicate, a signal-only request, and an ordered cursor with a custom comparator all retain their row through QueryClient dehydration, reconstruction of Seroval's emitted stream payload, and Query Core hydration from that payload. The on-demand query functions still receive the original IR, comparator, and signal; serialized metadata retains enumerable user metadata but excludes request options. The original implementation fails the compound and signal cases at the stream checkpoint. A serializable stream-only request-options leak passes the former chunk-count check but fails the current payload assertion. This owner covers successful Query cache entries at the installed Query Core 5.90.20 and Seroval 1.5.0 versions. A TanStack Start integration owner still needs the reporter's Router 1.171.33 and Seroval 1.6.8 path, browser Collection resume/refetch, and request cancellation across SSR. Query subset and pagination oracles remain owners of predicate/order/cursor meaning and supported value types. The issue's `shouldDehydrateQuery` workaround replaces Query Core's success-only default and admits pending/error queries; any documented workaround must preserve that default filter. | -| Ordered acquisition | [pagination](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pagination-oracle.property.test.ts), [ordered work](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-work-oracle.property.test.ts), [ordered lifecycle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts), [ordered loader state](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-source-loader-state.test.ts), [graph scheduler](https://github.com/TanStack/db/blob/main/packages/db/tests/query/scheduler.test.ts), [issue #1880 ordered-repair review](oracle-reviews/issue-1880-ordered-repair.md), [issue #1882 custom local collation review](oracle-reviews/issue-1882-custom-local-collation.md), [issue #1898 relation-filter review](oracle-reviews/issue-1898-relation-filter.md), [PR #1909 external review](oracle-reviews/pr-1909-external-review.md) | Complete finite provider results, inherited locale and bounded/unbounded custom collation with exact request options, query-level inheritance across source aliases, real lexical/numeric disagreement, custom comparator ties, scan/index paths, pending windows, ties/nulls, lease ownership, Effect callback gates, including an indexed source update during Effect startup, and documented repair timing. Direct LEFT-joined filters cover local finite prefixes, pending child demand, child removal, and anti-join publication with custom full-source and unindexed fallback loading; unrelated include demand cannot stall the root continuation, and an empty joined replay wakes held Effect callbacks; remote relation hints remain outside this owner. Graph scheduler checks coalescing, dependency order, synchronous loader input, and repeated-alias failure ownership. The ordered-work oracle checks that a synchronous root refill reaches D2 before joined publication; a mutant that omitted the post-loader graph step failed at the second child demand. Custom collation proves local full-source acquisition only; it does not establish provider cursor capability or backend collation fidelity. Sibling-source input during a synchronous ordered continuation returns to graph work before another ordered acquisition. Request completion is not proof of unrequested source extent. A window move during existing joined demand waits for its root continuation, including an independently held root request; current demand failure rejects the move, while retirement and an obsolete rejection do not. A bounded ordered repair resumes its refill after joined demand settles. The ordered-source-loader state test checks that an explicit failed-request retry blocked by joined demand retains its generation until it can issue a request. Multiple simultaneously pending joined plans during a window move and a public-query failed-retry overlap still need dedicated witnesses. | -| Indexed predicate filtering | `packages/db/tests/query/index-path-collision-oracle.test.ts` | A bounded cross product of numeric values checks exact public keys before and after adding a nested-field BTree index. Direct predicates vary argument order, bound inclusivity, reversed operands, and bound field. Public Collection subscription and live-query callbacks run with and without the index. Selected-field ordering compares a direct JavaScript sort with a public `$selected` callback; restoring the dotted-key proxy cache made that comparison fail. The original compound grouping failed four indexed cases while scan controls passed. The original callback proxy caches also failed both unindexed public routes. A hostile cleanup control preserves the primary mismatch and secondary release error. Arbitrary path segments, nullish values, custom collation, and incremental publication need separate witnesses if claimed. | -| Lazy target path identity | `packages/db/tests/query/compiler/lazy-targets.test.ts` | A focused same-source `UnionFrom`/`coalesce` witness requires both [`a.b`] and [`a`, `b`] demand targets. Restoring dotted-string deduplication drops the second target at the compiler boundary. A public on-demand adapter and row-publication history still need a separate witness. | -| Correlated include path identity | `packages/db/tests/query/includes-context-transport-oracle.test.ts` | A flat parent field and nested parent field reach one-level and nested `toArray` results independently; dotted ancestor aliases remain distinct through a grandchild route. Initial results and parent updates have exact public-value checks. A conditional projection gives [`a.b`] and [`a`, `b`] different include results and checks parent-key changes plus later child inserts. Removing the compiler's unique route-key allocator loses a public child result; restoring dotted-string deduplication at either builder site loses a distinct parent value. These fixed witnesses do not establish arbitrary path segments, every recursive source form, or every materialization form. | -| Join equality and cold acquisition | `packages/db/tests/query/cold-join-reconciliation-oracle.test.ts` | Independent recomputation for cold acquisition plus direct join/predicate equivalence across established equality domains. Binary/string and nullish classes, replacement histories, raw on-demand values, and both scan/auto-index paths are explicit; compound join syntax is not claimed. | -| Optimizer aggregate pushdown | `packages/db/tests/query/optimizer-semantics-oracle.test.ts`, [review record](oracle-reviews/2026-09-28-optimizer-aggregate-pushdown.md), [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | Independent sum recomputation and a materialized-Collection formulation check the first public snapshot of a nested aggregate under a left join. Direct, arithmetic-wrapped, and conditional aggregates distinguish safe from unsafe pushdown; a grouped-key control covers the grouped boundary. Matching and nonmatching global aggregates also cross a nested QueryRef inner join with absent, accepting, and rejecting outer predicates. The matching initial cases fail on the pre-repair revision and pass after the computed-projection lookup repair. Other predicates, wrappers, join forms, and incremental optimizer histories remain outside this owner. | -| QueryRef operators and user-value boundaries | `packages/db/tests/query/subquery-user-value-oracle.test.ts`, `packages/db/tests/transactions.test.ts`, [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | A finite array/Set model checks the compiled output bag for a joined DISTINCT subquery with an outer WHERE. Live-query drivers check selected containers, no-select, functional, and predicate-literal user objects with IR-like fields, top-level and aggregate-subquery proxy-shaped fields, arrays of selected references, outer virtual-field filters on joined DISTINCT and nested aggregate QueryRefs, and a renamed no-select source. The transaction suite documents the existing development-browser duplicate-load guard. The bounded cases run in `test:oracles` or the full DB suite. Joined DISTINCT, nested and flat aggregate source-update histories pass. Ordered joined `findOne()` checks initial, singleton, empty, restored, and changed-join-key cuts; an unordered `findOne()` QueryRef on either side of a join checks default-key candidate deletion and reinsertion. A materialized joined `findOne()` supplies a separate receiving formulation. Other `singleResult` forms, join forms, schedules, and cross-copy value handling outside the duplicate-load guard remain unproved. | -| Opaque backend pagination | [window oracle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.oracle.test.ts), [cache histories](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.cache-oracle.test.ts), [cache publication](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.publication-oracle.test.ts), [browser acquisition boundaries](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.boundary-oracle.test.ts), [QueryCollection integration](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.integration.test.ts) | Full filter/sort/slice reference, opaque token transport, actual Query cache expiry/invalidation/GC, forced refresh during growth, protocol failure publication/recovery, bounded slice work, nested cancellation/replacement, reader abort, browser retry defaults, manual-write cache isolation, and production window publications. Stable backend sequences; not snapshot guarantees for changing endpoints. Peek-ahead remains enabled. | -| Electric and TrailBase | [Electric histories](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/tests/electric-oracle.property.test.ts), [recovery histories](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/tests/electric-recovery-oracle.test.ts), [held resume snapshots](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/tests/electric-resume-snapshot-races.test.ts), [PostgreSQL semantics](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/e2e/sql-predicate-semantics.e2e.test.ts), [TrailBase contract](https://github.com/TanStack/db/blob/main/packages/trailbase-db-collection/tests/ORACLE.md) | Installed SDK delivery/framing, independent predicates, exact subscription arguments, restart/reset lineage, held certification and durability races, source-order publication before durability, and late errors. The queued-presence property runs identical fixed/random generators plus isolated seed-and-path replay across insert, update, delete, and truncate callbacks. The recovery fixtures use a mocked ShapeStream; they do not establish live Electric-service framing or native persistence-host behavior. | -| PowerSync | [tests](https://github.com/TanStack/db/tree/main/packages/powersync-db-collection/tests), `tests/correctness-oracle.test.ts` | Applied receipt positions crossed with held peers, native SQLite/SDK and cleanup evidence. A metadata-bearing `Collection.update` passes through an observed SDK watcher callback to the public and SQLite rows and an exact queued CRUD patch. Held callback ordering and synthetic zero-field cases remain controlled-only, and remote upload has no receiving fixture here. Run the focused owner with the package's `test:oracles` command. A timeout mutant proves a progress failure, not every value assertion. | -| SQLite persistence and native hosts | [persisted histories](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/persisted.test.ts), [reset/resume histories](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/sqlite-core-adapter.test.ts), [dual-adapter resume snapshots](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/sqlite-resume-snapshot.test.ts), [Browser composed-owner histories](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/per-collection-coordinator-oracle.test.ts), [Browser coordinator RPC](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/browser-coordinator.test.ts), [shared-driver fairness](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/shared-driver-fairness-oracle.test.ts), [driver contracts](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts), [Node shared-handle scheduling](https://github.com/TanStack/db/blob/main/packages/node-db-sqlite-persistence/tests/node-driver.test.ts), [OP-SQLite shared-handle scheduling](https://github.com/TanStack/db/blob/main/packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts), [browser OPFS lifecycle](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/opfs-page-lifecycle-oracle.test.ts), [worker diagnostics](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/opfs-worker-diagnostics-oracle.test.ts), [Electron IPC and composed owner](https://github.com/TanStack/db/blob/main/packages/electron-db-sqlite-persistence/tests/electron-ipc.test.ts), [113-law manifest](https://github.com/TanStack/db/blob/main/packages/db-collection-e2e/src/fixtures/persisted-conformance-manifest.ts) | Core cache/remote rejection/peer/reopen histories, atomic reset/resume lineage, key-set evidence, dual-adapter races, queued coordinator reload/gap notifications during startup, unscheduled startup/reset overlap, and exact driver results. The shared core-adapter contract checks custom local comparator dispatch for ascending, descending, and comparator-equal public-key ordering at `loadSubset` completion. Host packages register this contract, but the local sqlite3 CLI run does not prove native host execution. Browser composes public source commits with per-collection elected-owner routing and covers the complete committed-transaction wire partition through deterministic Node transport seams. Remote-subset histories distinguish logical demand, physical acquisitions, exact acquisition leases, and released replay tombstones. Electron composes source commits with a per-collection renderer owner, IPC adapter, real SQLite, and reopen checks. The shared-driver fairness owner checks K=1 complete-logical-hydrate scheduling with identical fixed, random, and seed-plus-path campaigns. It records public rows, raw dequeue reach, and a persist-first FIFO hostile control. Its Chromium OPFS fixture refines the provider boundary but does not establish a browser matrix, elapsed-time latency, unbounded eventuality, or multi-process coordination. Same-handle Node and OP-SQLite tests cover transaction admission. Controlled OPFS page/worker histories cover ownership and diagnostic-cause retention. The reset/resume owners use sqlite3 CLI and in-memory node:sqlite seams; they do not prove multi-process WAL, mobile/Tauri, or other native-device execution. Distinct database handles rely on SQLite lock admission rather than one in-process queue. React Native hosts without async-context propagation must use the transaction driver supplied to the callback for nested work. Fake workers and synthetic page events do not prove native handle release or real bfcache admission. The Browser composed seams are not real multi-context/OPFS-worker execution; the Electron harness is not an actual Electron process unless its explicit runtime-bridge mode runs. An ownerless elected node suppresses core routing, while a follower may route demand to the elected owner; host coordinators retry only classified transport or admission failures while demand remains retained. The manifest excludes progressive and move suites; registration and shim runs are not device execution. | -| SQLite expression-index planning | [Node expression-index oracle](https://github.com/TanStack/db/blob/main/packages/node-db-sqlite-persistence/tests/expression-index-oracle.test.ts) | RFC #1659 invariant 8 owns identical persisted-index and runtime-expression shapes. Independent expected keys are checked against direct captured SQL, adapter results, and named-index plans. Generated BigInts use SQLite's signed range; one fixed case checks legacy oversized-value reads. Other limits: bounded unqualified JSON paths/scalars, Node BetterSQLite, and no null, arbitrary raw SQL, or native-host planning. Run the package's `test:oracles` campaign. | -| Offline execution | [scheduler](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/KeyScheduler.property.test.ts), [leadership](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/leadership-replay.property.test.ts), [settlement](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/transaction-settlement.property.test.ts), [serialization](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/transaction-serializer.property.test.ts), [web connectivity replay](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/connectivity-replay-oracle.test.ts), [review record](oracle-reviews/pr-1879-visible-replay.md) | Declarative FIFO eligibility, per-transaction outcomes, durable state and typed wire trees. The web connectivity owner checks a persisted write under a false browser online hint, then visible replay by event or explicit retry, at provider-call, outbox, and caller-settlement cuts. It uses controlled browser globals and fake storage; it does not prove actual browser network detection, genuine offline retry timing, multi-tab leadership transfer, or React Native behavior. Issued work may finish after ownership loss, but new work must not start. Exactly-once network execution is not promised. | -| Frameworks | [React conformance](https://github.com/TanStack/db/blob/main/packages/react-db/tests/conformance.test.tsx), [React pagination](https://github.com/TanStack/db/blob/main/packages/react-db/tests/infinite-query-conformance.test.tsx), [shared suites](https://github.com/TanStack/db/tree/main/packages/db-collection-e2e/src/suites), [Vue synchronous publication](https://github.com/TanStack/db/blob/main/packages/vue-db/tests/useLiveQuery-publication-oracle.test.ts) | Exact exposed rows/pages and each framework's own lifecycle cuts. Vue's synchronous watcher checks insert, update, and nonterminal delete through supplied Collection and identity query inputs; it does not cover an empty final result, multiple changes in one transaction, or query recompilation. A React witness does not prove Vue/Solid/Angular/Svelte scheduling. Preserve their receiving registrations. | -| Window-controller overlap | [shared infinite-query conformance](https://github.com/TanStack/db/blob/main/packages/db/tests/conformance/infinite-suite.ts), [React driver](https://github.com/TanStack/db/blob/main/packages/react-db/tests/infinite-query-conformance.test.tsx), [Vue driver](https://github.com/TanStack/db/blob/main/packages/vue-db/tests/infinite-query-conformance.test.ts), [Svelte driver](https://github.com/TanStack/db/blob/main/packages/svelte-db/tests/infinite-query-conformance.svelte.test.ts), [review record](oracle-reviews/dec-03-window-overlap.md) | Four or five ordered source rows, two window-settlement orders, and public snapshots after each settlement, a subscribed observer update, a detached controller read, and explicit recovery. Insertion and removal of the fifth row distinguish both continuation directions while an earlier error remains visible. The detached read follows a source change with no controller subscriber; its preloaded Collection retains the five-row window. The exact overlap uses the exported DB controller in each package realm. Framework hooks expose no controller preload, so this cell does not establish a hook scheduling path; direct hook paging remains in the adjacent shared scenarios. An unsubscribed controller has no notification claim. | -| Structural values and ordered primitives | [hash values](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash.property.test.ts), [hash identity](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-identity-oracle.property.test.ts), [MultiSet consolidation](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/multiset-consolidate-oracle.property.test.ts), [hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-graph.property.test.ts), [mixed hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-mixed-graph.property.test.ts), [hash retry](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-failure-retry.property.test.ts), [comparison](https://github.com/TanStack/db/blob/main/packages/db/tests/comparison.property.test.ts), [deep equality](https://github.com/TanStack/db/blob/main/packages/db/tests/utils.property.test.ts), [cursor](https://github.com/TanStack/db/blob/main/packages/db/tests/cursor.property.test.ts), [indexes](https://github.com/TanStack/db/blob/main/packages/db/tests/index-update.property.test.ts), [BTree Map](https://github.com/TanStack/db/blob/main/packages/db/tests/btree-map-oracle.test.ts), [query identity](https://github.com/TanStack/db/blob/main/packages/db/tests/query/identity-output-shape-oracle.test.ts), [LIKE semantics](https://github.com/TanStack/db/blob/main/packages/db/tests/query/compiler/evaluators.test.ts) | Independent flat values, graph topology, algebraic laws, Map/group/sort recomputation, expression denotation, custom comparator dispatch/reference identity/index compatibility, LIKE wildcard refinement, compiled output bags, declared value-pair agreement between D2 equality keys and IR equality operands, and Collection collation with exact public scan/index order and one-index reuse across repeated reads. The auto-index cells cross BasicIndex and BTreeIndex; scan cells omit indexes. Both paths cover lexical and numeric `en-US` locale defaults plus an explicit ordinary locale override of a numeric default. An unindexed six-row work cell bounds Collection collation resolution to at most two reads per ordered scan: one index-path check and one scan clause. Other locales and ordered histories are outside this owner. The identity pair grammar covers primitives, signed zero, NaN/invalid Date, valid Date/timestamp, binary/Buffer, Temporal, nested references, symbols, and functions; fixed and sampled pairs do not prove every JavaScript value or hash collision freedom. Ref proxies are expressions rather than literal values in this lane. The hash-values owner samples Set/object and Map/object type-marker distinctions in fixed and random campaigns with seed-and-path replay; collision freedom is not promised. The hash-identity owner checks `hash` and `equalHashValues` against one spec-level identity model: primitives with signed zero, NaN, and bigint; reference leaves (functions, registered handles, `File`, binary over 128 bytes); Date, binary, Temporal, and RegExp headers; array length and holes; Map and Set insertion order; and object keys in any order, for acyclic values and for cycles through arrays, Map values, Sets, and plain objects. Map/Set order sensitivity is pinned current behavior, not a promise. Arrays from another realm, RegExps with a `NaN` `lastIndex`, getters, proxies, other cycle carriers, and pairs that differ only in a back-edge target are outside its grammar; pinned cases keep equality's cross-realm length check and strict RegExp field comparison. `hash-work.test.ts` pins how visited values and array/RegExp headers count toward the structural work cap, and that equality copies a shared Map's entries once per distinct pair ([code-weight hash dispatch review](oracle-reviews/code-weight-hash-dispatch.md)). Custom string indexes do not optimize ordinary ranges, and custom cursors remain unsupported. The LIKE owner covers boolean string matching and bounded work, not nullish three-valued logic or a general Unicode collation contract. Unsupported composite cursors reject. The MultiSet consolidation owner checks keyed, single-type, and structural identity, signed zero and NaN in keyed and single-number data, the retained first record, zero-sum removal, and input record contents. It does not assert output order. Its keyed grammar includes cross-type key and value collisions, non-finite numeric keys, the `\|` delimiter, symbol and function identity, and a direct-object/object-tuple delimiter control from #1948. Eight named collision witnesses and both generated campaigns are RED on `c879ba6d` and GREEN with the repair ([#1948 review record](oracle-reviews/issue-1948-keyed-consolidation.md)); the [code-weight consolidation review](oracle-reviews/code-weight-multiset-consolidation.md) records the earlier exclusion. The keyed reference uses direct value/reference equality, including SameValueZero for numeric keys. The unkeyed fallback keeps its original key and value domains. The direct `MultiSet` owner does not prove which `@tanstack/db` query shapes produce primitive keyed values or mixed-type source keys; that needs a live-query production witness. The index owner enforces one invalid-comparator law across both BasicIndex and BTreeIndex: for each comparator, accepted numeric prefix (including the empty prefix), and probe operation (add, update, eq/range lookup, take, build), the operation throws exactly when it receives a `NaN` or non-number result and never when every comparison is valid. A `signed infinity` comparator is the valid control, and a custom collation `compare` supplied through `compareOptions` is checked the same way. Two BTree Map split histories check inserted-key reads, exact whole-range payloads/order, size, and extrema, proving split placement follows the insertion index without a post-mutation comparison. A rejected index add or remove leaves the index refining its accepted rows; update and build are not atomic. At the Collection boundary, both index types and both write paths (optimistic insert, sync commit) crash the collection: status becomes `error` and the next mutation throws, so a usable collection never holds rows its subscribers were not told about. Open: a sync source can still commit writes on a collection in `error` state, which stores unpublished rows; this is existing `markError` behavior and needs a lifecycle-owner witness. Comparator transitivity is outside this evidence. | -| WHERE predicate publication | [WHERE predicate publication oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts) | An independent Kleene evaluator judges `eq`/`not`/`and`/`or` predicates over strings, a normalization-prefixed string, booleans, numbers, `NaN`, a valid Date, `null`, a missing field, and virtual fields. Snapshot histories with pending optimistic inserts compare a live query, direct subscribers with and without initial state, and `currentStateAsChanges`; change histories apply multi-key sync transactions, including reinsertion of a key deleted earlier, and compare every consumer's key set after each commit, on scan and `BasicIndex` paths. Peer subscribers share one field with different literals, plus one on a virtual field, so every published change must reach exactly the subscribers whose predicate it can satisfy; routing mutants that ignored the previous value, ignored the field, delivered a change twice, or routed while stale published rows awaited reconciliation fail here. Fixed witnesses cover a layout-only publication reaching a filtered subscriber as one empty batch, a filtered subscriber's empty Collection-readiness batch, retraction of a vanished row after eager cleanup and restart, and the new row plus exact subscriber update payload when a same-key row stays TRUE. A stale-live-value mutant survives the generated membership checks but fails the payload witness. A focused [property-visibility test](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-prefilter-property-visibility.test.ts) compares the public unindexed snapshot with the enriched-row contract for inherited, non-enumerable, enumerable-own, and nested getter paths; the pre-fix stored-row shortcut failed three of its four controls. Other property-descriptor and stateful-getter histories remain outside those fixed controls. Hostile mutants for FALSE-for-UNKNOWN `eq`, `or` or number-literal subscription prefilters, a prefilter that ignores the previous value, a skipped readiness batch, a skip while stale published rows await reconciliation, and an unindexed snapshot scan that reads only synced rows while an optimistic insert or delete changes visibility passed the prior `@tanstack/db` suite and fail here. Change routing withholds a dropped row from an `eq` subscription before its sent key could be recorded, so a mutant that records dropped rows as sent is equivalent for routed predicates in this oracle. A focused `collection-subscription.test.ts` witness checks that a dropped insert or update, alone or beside a matching change, cannot advance a limited subscription's page offset under a routed `eq` and an unrouted `or` predicate; the unrouted cases beside a matching change kill that mutant, and the unrouted `alone` case does not. A loose-equality prefilter is an equivalent mutant: it only skips less. Skipping during truncate replay also survives; the replay's baseline diff re-derives the same retraction, so the guard keeps the prior dataflow without its own witness. Generated cleanup and restart histories for filtered subscribers remain open for the lifecycle publication owner. No-op updates, other comparison operators, Temporal and binary operands, joins, ordering, optimistic updates, and truncate are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. | -| Joined result keys | [Joined result key oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/join-result-key-oracle.property.test.ts) | A nested-loop model recomputes the pair set of an inner, left, or full join over source keys drawn from plain, comma-bearing, bracket-bearing, and quoted strings, numbers beside the strings that print the same, both infinities, and `NaN`. After preload and each synced group change, the published rows must equal the model's pairs and the result key count must equal the row count. The comma-joined key encoding fails its two pinned histories and both campaigns; plain `JSON.stringify`, which prints infinities as the missing-side `null`, fails the pinned infinity history and both campaigns. A pinned history in which a `NaN`-keyed pair leaves and re-forms fails when the join index compares source-key prefixes with `===`. Joins over subqueries, more than two sources, custom `getKey`, and optimistic mutations are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. | -| D2 Index storage | [Index refinement oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/index-refinement-oracle.property.test.ts) | A plain-`Map` model sums multiplicities under a type-and-value identity in which `NaN` equals `NaN` and `-0` equals `0`. Histories of up to twelve additions to two keys mix prefixed arrays with `NaN`, `0`, `-0`, `1`, `'1'`, `1n`, and `'a'` prefixes and unprefixed values, with cancellation and reappearance. After each addition, `get` and `has` must equal the model. Comparing prefixes with `===`, which disagrees with the `Map` that holds them, fails three pinned histories and both campaigns; the `-0` control passes under both. Compaction, presence tracking, and structural payloads are outside this owner; the join operator tests and incrementalization law own operator behavior. | -| Index suggestions | [collection-size suggestion oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/index-suggestion-oracle.test.ts) | A public filtered live-query Collection checks manual/default, eager, threshold, matching-index, and unrelated-index cases against the documented collection-size suggestion policy. The original manual-silence/eager-noise implementation failed two expected observations. The owner does not judge repeated-query warning volume, slow-query timing, query work, production-build suppression, or every query shape. It runs in `@tanstack/db`'s `test:oracles` campaign. | -| Minified DB public API | [consumer bundle check](https://github.com/TanStack/db/blob/main/scripts/test-minified-db.mjs) | CI builds `@tanstack/db`, then bundles its built `dist` entry with db-ivm inlined through esbuild `minify: true`. The build renames TypeScript-private members from `packages/db/mangle-cache.json`, so this lane runs against the renamed output that consumers install. It rejects a `dist` that is older than `src`, the cache, or the package's build configuration. `pnpm --filter @tanstack/db test:dist` also runs five db test files against the built `dist`, chosen for coverage of the modules with the most renamed members. `pnpm check:mangle` fails when a cached name is used other than as a private member in db src, is read by another package, appears as a string, or has a short name that collides with a source identifier ([private-member mangling review](oracle-reviews/code-weight-private-member-mangling.md)). It checks every exported error class's current public `name`, the built-in `BasicIndex` resolver name in index metadata and its event, complete query rows (after removing the four documented virtual fields) against an independent array filter/sort/projection, and one live update. The `new.target.name`, public-member mangle, and extra-output-field calibration modes fail at the intended observations. This fixed slice does not replace the unminified generated oracles, exercise framework adapters, or establish stable names for custom index resolvers. | -| Boundary refinements | [cleanup/restart](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-cleanup-restart-oracle.test.ts), [issue #1891 async-cleanup review](oracle-reviews/issue-1891-async-cleanup.md), [issue #1891 cleanup-start follow-up](oracle-reviews/issue-1891-cleanup-start-followup.md), [metadata publication](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-metadata-publication-oracle.property.test.ts), [state retention](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-state-retention-oracle.property.test.ts), [acquisition cells](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-oracle.test.ts), [D2 source reconciliation](https://github.com/TanStack/db/blob/main/packages/db/tests/d2-source-reconciliation-oracle.property.test.ts), [top-K support windows](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts), [nested Query work](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts) | Explicit lifecycle products, independent source maps and weighted relations, exact publication cuts, support/multiplicity, and value-plus-work observations. These refine the larger subsystem models; they do not replace them. | -| Small structures and test mechanics | [SortedMap](https://github.com/TanStack/db/blob/main/packages/db/tests/SortedMap.test.ts), [cleanup queue](https://github.com/TanStack/db/blob/main/packages/db/tests/cleanup-queue.property.test.ts), [live-query GC clock](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-gc-clock.test.ts), [guarded replay](https://github.com/TanStack/db/blob/main/packages/db/tests/oracle-replay.test.ts) | Map/full-sort and elapsed appointment-list models with executed target/seed/path checks. SortedMap also checks deferred bulk writes, array-movement work, scan reuse, live iterators, runtime `undefined` keys, and public single-transaction and queued-transaction Collection sync paths. It does not settle the contract for comparator-observed values mutated after publication or every legal iterator/mutation interleaving. Wall-clock steps do not move due times, and a focused public witness checks source subscription release. Callback-reentrant scheduling, OS suspend/resume, environments without `performance.now()`, and fake/real Performance clock replacement while GC is pending remain outside this coverage. Tests must drain or reset pending GC before switching timer providers. | +| Surface | Primary executable owners | Independent judgment and important limit | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ordered relations and BTree | [top-K relation oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/topk-relation-oracle.test.ts), [fractional-index window moves](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/topKWithFractionalIndex.test.ts), [BTree/Map](https://github.com/TanStack/db/blob/main/packages/db/tests/btree-map-oracle.test.ts), [incrementalization laws](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/incrementalization-law.property.test.ts) | Independent ordered relations and cumulative signed output. Top-K consolidation compares same-key values without hashing, including cyclic replacements and fresh transient cancellation. The fractional-index test checks an empty output message for a window move after all rows retract; it does not prove downstream lazy acquisition. Other hash-based operators retain hashing's declared domain. Algebra does not specify client readiness. The BTree/Map oracle checks point operations, neighbor pairs, and full, partial, boundary, exclusive-high, and empty range scans for integer keys and node sizes 4–8. It does not check balance or space, and it assumes consistent comparators ([code-weight BTree review](oracle-reviews/code-weight-btree-trim.md)). | +| Includes and publication | [cross-formulation](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts), [temporal](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-temporal-oracle.test.ts), [Collection includes](https://github.com/TanStack/db/blob/main/packages/db/tests/query/includes-collection-oracle.property.test.ts), [architecture and complete suite map](https://github.com/TanStack/db/blob/main/packages/db/src/query/live/ARCHITECTURE.md#executable-contracts) | Per-parent/flat-join/partition relations, callback-time rows, nested values, and route histories. Fragmented lazy-demand consolidation covers delta growth and successful, rejected/retried, and obsolete replacements at the Collection request boundary; it observes exact key unions and abort checkpoints. A compiled-includes fixture makes adapter unload evict owned child rows and proves a rejected union cannot remove established public child rows. Retry after that terminal live-query failure remains covered at the acquisition boundary; no automatic in-place recovery is claimed. Observe raw promised order; fresh queries do not establish continuous publication safety. | +| Collection lifecycle | [mutation startup](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-mutation-startup-oracle.test.ts), [change-event history](https://github.com/TanStack/db/blob/main/packages/db/tests/change-event-history-oracle.test.ts), [history](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-history.property.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-publication.property.test.ts), [replay](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-replay-oracle.property.test.ts), [effect disposal](https://github.com/TanStack/db/blob/main/packages/db/tests/effect-disposal-oracle.test.ts), [PR #1902 review](oracle-reviews/pr-1902-change-event-history.md), [batch-order decision review](oracle-reviews/change-event-batch-order.md) | Core Collection `insert`/`update`/`delete` admission while `startSync:false` is idle; complete public-row/change-message agreement across bounded one- and two-key histories plus replayable four-key campaigns; a two-key deferred sync history checks each key's causal insert/update/delete trace while allowing any cross-key interleaving. Reversing all deferred messages fails that check; reversing each sync transaction's distinct-key messages passes. Queued duplicate admission and cancellation, ownership and phase histories, exact caller/error/publication evidence, and late completion and restart have separate witnesses. Generated deferred histories with cancellation or reentrant callbacks need a witness in the change-event history owner before claiming general batch-shape coverage. Query write utilities and effect self-dependent disposal remain separate contracts. | +| Paced mutations | [virtual-clock oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/paced-mutations-oracle.test.ts) | `createPacedMutations` with queue front/back options, bounded waiting capacity at zero and one, cleanup draining in FIFO/LIFO order after a held write, cleanup timing at the configured wait for immediate writes, admitted writes after separate Collection cleanup, debounce and throttle schedules with explicit and omitted options, and first-leading throttle execution at epoch zero. Finite histories compare optimistic rows, returned transaction identity and receipt outcome, execution order and virtual-clock time. Held writes verify queue serialization. Leading-only throttle and debounce witnesses reject skipped calls immediately with their named dropped-call errors, including omitted edges, both edges disabled, and same-row rollback while a prior write is held. Capacity witnesses cover overflow rejection, distinct-key optimistic rollback, same-key admitted-write survival, and in-flight versus waiting admission. Cleanup witnesses check repeated queue cleanup, eventual admitted receipt settlement, direct queue strategy rejection of new callbacks, public post-cleanup rollback with `QueueDisposedError`, pending debounce transactions that wait until the last call's quiet edge after separate Collection cleanup, and a trailing throttle timer that runs at its regular edge after separate Collection cleanup. Deferred custom queue and batch callbacks check compatibility with `void` and `false` execute results, respectively. Frozen options verify factory non-mutation; a mutable option witness checks that debounce uses its construction-time trailing setting. New debounce/throttle admission after cleanup, failed persistence, broader capacities and schedules remain outside this owner. | +| Optimistic state | [history model](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-oracle.ts), [generated histories](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-transaction-oracle.property.test.ts), [truncate capture ownership](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-truncate-ownership-oracle.property.test.ts), [outcomes](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-outcomes.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-publication.test.ts) | Independent whole-row snapshots, rollback dependencies, captured truncate ownership, metadata and prior-value events. Never rebase a pending snapshot merely to simplify the model. | +| Drafts and native values | [proxy](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy.test.ts), [detachment](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-detachment-contract.test.ts), [iteration](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-iteration-contract.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. Set and Map reorder histories check draft patches and stored iteration order after replacement and clear-and-readd; RegExp replacement/reversion histories check `lastIndex`. General native-mutator and symbol-write support is not established by a plain-object oracle. | +| Query DB and observer | [ownership](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts), [load lifecycle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/load-subset-lifecycle-oracle.test.ts), [observer histories](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-observer-history.property.test.ts) | Real QueryClient boundary, applied-settlement barriers for existing and cached demand, mutation-handler Collection identity, terminal fetch-record lifecycle, and a per-listener eligibility ledger, not a duplicate dispatch queue. The bounded handler grammar crosses insert, update, delete, parameter and mutation-alias access, refetch, and clearError. A held clearError retry retains the prior public error through initial, background, and Collection application failures; success clears it, failure advances the count, including a repeated error at clock time zero. An intermediate Query retry attempt does not recount the previous background error; a held final attempt can succeed or fail. A held application after successful Query fetch keeps the error visible until the applied result. A mocked persisted-baseline scan holds an older success while a newer terminal Query failure arrives; the newer public error and count survive application, including when the Error object and clock tick repeat. Native SQLite persistence, concurrent retries across multiple tracked Queries, mutation-handler fetch-boundary settlement, and deferred result application remain outside this witness. The mutation overlap grammar crosses both start orders. Check reentry, peer survival, FIFO and disposal independently of final rows. | +| Query DB SSR dehydration | `packages/query-db-collection/tests/ssr-dehydration-oracle.test.ts`, [review record](oracle-reviews/issue-1950-ssr-dehydration.md) | A plain Query cache control, a live on-demand compound predicate, a signal-only request, and an ordered cursor with a custom comparator all retain their row through QueryClient dehydration, reconstruction of Seroval's emitted stream payload, and Query Core hydration from that payload. The on-demand query functions still receive the original IR, comparator, and signal; serialized metadata retains enumerable user metadata but excludes request options. The original implementation fails the compound and signal cases at the stream checkpoint. A serializable stream-only request-options leak passes the former chunk-count check but fails the current payload assertion. This owner covers successful Query cache entries at the installed Query Core 5.90.20 and Seroval 1.5.0 versions. A TanStack Start integration owner still needs the reporter's Router 1.171.33 and Seroval 1.6.8 path, browser Collection resume/refetch, and request cancellation across SSR. Query subset and pagination oracles remain owners of predicate/order/cursor meaning and supported value types. The issue's `shouldDehydrateQuery` workaround replaces Query Core's success-only default and admits pending/error queries; any documented workaround must preserve that default filter. | +| Ordered acquisition | [pagination](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pagination-oracle.property.test.ts), [ordered work](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-work-oracle.property.test.ts), [ordered lifecycle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts), [ordered loader state](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-source-loader-state.test.ts), [graph scheduler](https://github.com/TanStack/db/blob/main/packages/db/tests/query/scheduler.test.ts), [issue #1880 ordered-repair review](oracle-reviews/issue-1880-ordered-repair.md), [issue #1882 custom local collation review](oracle-reviews/issue-1882-custom-local-collation.md), [issue #1898 relation-filter review](oracle-reviews/issue-1898-relation-filter.md), [PR #1909 external review](oracle-reviews/pr-1909-external-review.md) | Complete finite provider results, inherited locale and bounded/unbounded custom collation with exact request options, query-level inheritance across source aliases, real lexical/numeric disagreement, custom comparator ties, scan/index paths, pending windows, ties/nulls, lease ownership, Effect callback gates, including an indexed source update during Effect startup, and documented repair timing. Direct LEFT-joined filters cover local finite prefixes, pending child demand, child removal, and anti-join publication with custom full-source and unindexed fallback loading; unrelated include demand cannot stall the root continuation, and an empty joined replay wakes held Effect callbacks; remote relation hints remain outside this owner. Graph scheduler checks coalescing, dependency order, synchronous loader input, and repeated-alias failure ownership. The ordered-work oracle checks that a synchronous root refill reaches D2 before joined publication; a mutant that omitted the post-loader graph step failed at the second child demand. Custom collation proves local full-source acquisition only; it does not establish provider cursor capability or backend collation fidelity. Sibling-source input during a synchronous ordered continuation returns to graph work before another ordered acquisition. Request completion is not proof of unrequested source extent. A window move during existing joined demand waits for its root continuation, including an independently held root request; current demand failure rejects the move, while retirement and an obsolete rejection do not. A bounded ordered repair resumes its refill after joined demand settles. The ordered-source-loader state test checks that an explicit failed-request retry blocked by joined demand retains its generation until it can issue a request. Multiple simultaneously pending joined plans during a window move and a public-query failed-retry overlap still need dedicated witnesses. | +| Indexed predicate filtering | `packages/db/tests/query/index-path-collision-oracle.test.ts` | A bounded cross product of numeric values checks exact public keys before and after adding a nested-field BTree index. Direct predicates vary argument order, bound inclusivity, reversed operands, and bound field. Public Collection subscription and live-query callbacks run with and without the index. Selected-field ordering compares a direct JavaScript sort with a public `$selected` callback; restoring the dotted-key proxy cache made that comparison fail. The original compound grouping failed four indexed cases while scan controls passed. The original callback proxy caches also failed both unindexed public routes. A hostile cleanup control preserves the primary mismatch and secondary release error. Arbitrary path segments, nullish values, custom collation, and incremental publication need separate witnesses if claimed. | +| Lazy target path identity | `packages/db/tests/query/compiler/lazy-targets.test.ts` | A focused same-source `UnionFrom`/`coalesce` witness requires both [`a.b`] and [`a`, `b`] demand targets. Restoring dotted-string deduplication drops the second target at the compiler boundary. A public on-demand adapter and row-publication history still need a separate witness. | +| Correlated include path identity | `packages/db/tests/query/includes-context-transport-oracle.test.ts` | A flat parent field and nested parent field reach one-level and nested `toArray` results independently; dotted ancestor aliases remain distinct through a grandchild route. Initial results and parent updates have exact public-value checks. A conditional projection gives [`a.b`] and [`a`, `b`] different include results and checks parent-key changes plus later child inserts. Removing the compiler's unique route-key allocator loses a public child result; restoring dotted-string deduplication at either builder site loses a distinct parent value. These fixed witnesses do not establish arbitrary path segments, every recursive source form, or every materialization form. | +| Alias scope identity | `packages/db/tests/query/includes-oracle.property.test.ts` (`includes alpha-renaming across sibling scopes`), `packages/db/tests/query/includes-scope-identity-oracle.ts`, `packages/db/tests/query/validate-aliases.test.ts`, [issue #1975 review](oracle-reviews/issue-1975-scope-identity.md) | Three topologies place a rewritable source beside a same-named sibling source: a `from()` subquery with an include, a joined `unionAll()` branch with an include, and a collapsed top-level pure wrapper with a join subquery. The grammar crosses zero to two joins, LEFT or INNER, zero to two predicates, plain, ordered-and-limited, or DISTINCT bodies, include forms, and eager or on-demand finite providers, after preload and after source writes. Includes read their source directly, through a join, through a `unionAll()` with an anchor join, or through a `from()` subquery that reads the parent row. One in four scenarios draws any naming and requires illegal ones to be rejected. Each checkpoint compares the canonical naming with plain recomputation, the generated naming with the canonical naming, and per-Collection `loadSubset` WHERE clauses. Every pinned witness and both campaigns fail on `18abceee4`; each of the four optimizer re-mint sites fails a witness when reverted. Nested includes, include-inside-subquery forms, join subqueries outside the wrapper topology, `groupBy`/`having`, outer RIGHT/FULL joins, publication events, unload, ordered windows and lazy join loading through the alias-merged `aliasRemapping`, and temporal demand remain outside this owner; they need witnesses before an alias-scope closure claim covers them. | +| Join equality and cold acquisition | `packages/db/tests/query/cold-join-reconciliation-oracle.test.ts` | Independent recomputation for cold acquisition plus direct join/predicate equivalence across established equality domains. Binary/string and nullish classes, replacement histories, raw on-demand values, and both scan/auto-index paths are explicit; compound join syntax is not claimed. | +| Optimizer aggregate pushdown | `packages/db/tests/query/optimizer-semantics-oracle.test.ts`, [review record](oracle-reviews/2026-09-28-optimizer-aggregate-pushdown.md), [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | Independent sum recomputation and a materialized-Collection formulation check the first public snapshot of a nested aggregate under a left join. Direct, arithmetic-wrapped, and conditional aggregates distinguish safe from unsafe pushdown; a grouped-key control covers the grouped boundary. Matching and nonmatching global aggregates also cross a nested QueryRef inner join with absent, accepting, and rejecting outer predicates. The matching initial cases fail on the pre-repair revision and pass after the computed-projection lookup repair. Other predicates, wrappers, join forms, and incremental optimizer histories remain outside this owner. | +| QueryRef operators and user-value boundaries | `packages/db/tests/query/subquery-user-value-oracle.test.ts`, `packages/db/tests/transactions.test.ts`, [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | A finite array/Set model checks the compiled output bag for a joined DISTINCT subquery with an outer WHERE. Live-query drivers check selected containers, no-select, functional, and predicate-literal user objects with IR-like fields, top-level and aggregate-subquery proxy-shaped fields, arrays of selected references, outer virtual-field filters on joined DISTINCT and nested aggregate QueryRefs, and a renamed no-select source. The transaction suite documents the existing development-browser duplicate-load guard. The bounded cases run in `test:oracles` or the full DB suite. Joined DISTINCT, nested and flat aggregate source-update histories pass. Ordered joined `findOne()` checks initial, singleton, empty, restored, and changed-join-key cuts; an unordered `findOne()` QueryRef on either side of a join checks default-key candidate deletion and reinsertion. A materialized joined `findOne()` supplies a separate receiving formulation. Other `singleResult` forms, join forms, schedules, and cross-copy value handling outside the duplicate-load guard remain unproved. | +| Opaque backend pagination | [window oracle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.oracle.test.ts), [cache histories](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.cache-oracle.test.ts), [cache publication](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.publication-oracle.test.ts), [browser acquisition boundaries](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.boundary-oracle.test.ts), [QueryCollection integration](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/cursor-pagination.integration.test.ts) | Full filter/sort/slice reference, opaque token transport, actual Query cache expiry/invalidation/GC, forced refresh during growth, protocol failure publication/recovery, bounded slice work, nested cancellation/replacement, reader abort, browser retry defaults, manual-write cache isolation, and production window publications. Stable backend sequences; not snapshot guarantees for changing endpoints. Peek-ahead remains enabled. | +| Electric and TrailBase | [Electric histories](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/tests/electric-oracle.property.test.ts), [recovery histories](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/tests/electric-recovery-oracle.test.ts), [held resume snapshots](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/tests/electric-resume-snapshot-races.test.ts), [PostgreSQL semantics](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/e2e/sql-predicate-semantics.e2e.test.ts), [TrailBase contract](https://github.com/TanStack/db/blob/main/packages/trailbase-db-collection/tests/ORACLE.md) | Installed SDK delivery/framing, independent predicates, exact subscription arguments, restart/reset lineage, held certification and durability races, source-order publication before durability, and late errors. The queued-presence property runs identical fixed/random generators plus isolated seed-and-path replay across insert, update, delete, and truncate callbacks. The recovery fixtures use a mocked ShapeStream; they do not establish live Electric-service framing or native persistence-host behavior. | +| PowerSync | [tests](https://github.com/TanStack/db/tree/main/packages/powersync-db-collection/tests), `tests/correctness-oracle.test.ts` | Applied receipt positions crossed with held peers, native SQLite/SDK and cleanup evidence. A metadata-bearing `Collection.update` passes through an observed SDK watcher callback to the public and SQLite rows and an exact queued CRUD patch. Held callback ordering and synthetic zero-field cases remain controlled-only, and remote upload has no receiving fixture here. Run the focused owner with the package's `test:oracles` command. A timeout mutant proves a progress failure, not every value assertion. | +| SQLite persistence and native hosts | [persisted histories](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/persisted.test.ts), [reset/resume histories](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/sqlite-core-adapter.test.ts), [dual-adapter resume snapshots](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/sqlite-resume-snapshot.test.ts), [Browser composed-owner histories](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/per-collection-coordinator-oracle.test.ts), [Browser coordinator RPC](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/browser-coordinator.test.ts), [shared-driver fairness](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/shared-driver-fairness-oracle.test.ts), [driver contracts](https://github.com/TanStack/db/blob/main/packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts), [Node shared-handle scheduling](https://github.com/TanStack/db/blob/main/packages/node-db-sqlite-persistence/tests/node-driver.test.ts), [OP-SQLite shared-handle scheduling](https://github.com/TanStack/db/blob/main/packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts), [browser OPFS lifecycle](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/opfs-page-lifecycle-oracle.test.ts), [worker diagnostics](https://github.com/TanStack/db/blob/main/packages/browser-db-sqlite-persistence/tests/opfs-worker-diagnostics-oracle.test.ts), [Electron IPC and composed owner](https://github.com/TanStack/db/blob/main/packages/electron-db-sqlite-persistence/tests/electron-ipc.test.ts), [113-law manifest](https://github.com/TanStack/db/blob/main/packages/db-collection-e2e/src/fixtures/persisted-conformance-manifest.ts) | Core cache/remote rejection/peer/reopen histories, atomic reset/resume lineage, key-set evidence, dual-adapter races, queued coordinator reload/gap notifications during startup, unscheduled startup/reset overlap, and exact driver results. The shared core-adapter contract checks custom local comparator dispatch for ascending, descending, and comparator-equal public-key ordering at `loadSubset` completion. Host packages register this contract, but the local sqlite3 CLI run does not prove native host execution. Browser composes public source commits with per-collection elected-owner routing and covers the complete committed-transaction wire partition through deterministic Node transport seams. Remote-subset histories distinguish logical demand, physical acquisitions, exact acquisition leases, and released replay tombstones. Electron composes source commits with a per-collection renderer owner, IPC adapter, real SQLite, and reopen checks. The shared-driver fairness owner checks K=1 complete-logical-hydrate scheduling with identical fixed, random, and seed-plus-path campaigns. It records public rows, raw dequeue reach, and a persist-first FIFO hostile control. Its Chromium OPFS fixture refines the provider boundary but does not establish a browser matrix, elapsed-time latency, unbounded eventuality, or multi-process coordination. Same-handle Node and OP-SQLite tests cover transaction admission. Controlled OPFS page/worker histories cover ownership and diagnostic-cause retention. The reset/resume owners use sqlite3 CLI and in-memory node:sqlite seams; they do not prove multi-process WAL, mobile/Tauri, or other native-device execution. Distinct database handles rely on SQLite lock admission rather than one in-process queue. React Native hosts without async-context propagation must use the transaction driver supplied to the callback for nested work. Fake workers and synthetic page events do not prove native handle release or real bfcache admission. The Browser composed seams are not real multi-context/OPFS-worker execution; the Electron harness is not an actual Electron process unless its explicit runtime-bridge mode runs. An ownerless elected node suppresses core routing, while a follower may route demand to the elected owner; host coordinators retry only classified transport or admission failures while demand remains retained. The manifest excludes progressive and move suites; registration and shim runs are not device execution. | +| SQLite expression-index planning | [Node expression-index oracle](https://github.com/TanStack/db/blob/main/packages/node-db-sqlite-persistence/tests/expression-index-oracle.test.ts) | RFC #1659 invariant 8 owns identical persisted-index and runtime-expression shapes. Independent expected keys are checked against direct captured SQL, adapter results, and named-index plans. Generated BigInts use SQLite's signed range; one fixed case checks legacy oversized-value reads. Other limits: bounded unqualified JSON paths/scalars, Node BetterSQLite, and no null, arbitrary raw SQL, or native-host planning. Run the package's `test:oracles` campaign. | +| Offline execution | [scheduler](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/KeyScheduler.property.test.ts), [leadership](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/leadership-replay.property.test.ts), [settlement](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/transaction-settlement.property.test.ts), [serialization](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/transaction-serializer.property.test.ts), [web connectivity replay](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/connectivity-replay-oracle.test.ts), [review record](oracle-reviews/pr-1879-visible-replay.md) | Declarative FIFO eligibility, per-transaction outcomes, durable state and typed wire trees. The web connectivity owner checks a persisted write under a false browser online hint, then visible replay by event or explicit retry, at provider-call, outbox, and caller-settlement cuts. It uses controlled browser globals and fake storage; it does not prove actual browser network detection, genuine offline retry timing, multi-tab leadership transfer, or React Native behavior. Issued work may finish after ownership loss, but new work must not start. Exactly-once network execution is not promised. | +| Frameworks | [React conformance](https://github.com/TanStack/db/blob/main/packages/react-db/tests/conformance.test.tsx), [React pagination](https://github.com/TanStack/db/blob/main/packages/react-db/tests/infinite-query-conformance.test.tsx), [shared suites](https://github.com/TanStack/db/tree/main/packages/db-collection-e2e/src/suites), [Vue synchronous publication](https://github.com/TanStack/db/blob/main/packages/vue-db/tests/useLiveQuery-publication-oracle.test.ts) | Exact exposed rows/pages and each framework's own lifecycle cuts. Vue's synchronous watcher checks insert, update, and nonterminal delete through supplied Collection and identity query inputs; it does not cover an empty final result, multiple changes in one transaction, or query recompilation. A React witness does not prove Vue/Solid/Angular/Svelte scheduling. Preserve their receiving registrations. | +| Window-controller overlap | [shared infinite-query conformance](https://github.com/TanStack/db/blob/main/packages/db/tests/conformance/infinite-suite.ts), [React driver](https://github.com/TanStack/db/blob/main/packages/react-db/tests/infinite-query-conformance.test.tsx), [Vue driver](https://github.com/TanStack/db/blob/main/packages/vue-db/tests/infinite-query-conformance.test.ts), [Svelte driver](https://github.com/TanStack/db/blob/main/packages/svelte-db/tests/infinite-query-conformance.svelte.test.ts), [review record](oracle-reviews/dec-03-window-overlap.md) | Four or five ordered source rows, two window-settlement orders, and public snapshots after each settlement, a subscribed observer update, a detached controller read, and explicit recovery. Insertion and removal of the fifth row distinguish both continuation directions while an earlier error remains visible. The detached read follows a source change with no controller subscriber; its preloaded Collection retains the five-row window. The exact overlap uses the exported DB controller in each package realm. Framework hooks expose no controller preload, so this cell does not establish a hook scheduling path; direct hook paging remains in the adjacent shared scenarios. An unsubscribed controller has no notification claim. | +| Structural values and ordered primitives | [hash values](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash.property.test.ts), [hash identity](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-identity-oracle.property.test.ts), [MultiSet consolidation](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/multiset-consolidate-oracle.property.test.ts), [hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-graph.property.test.ts), [mixed hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-mixed-graph.property.test.ts), [hash retry](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-failure-retry.property.test.ts), [comparison](https://github.com/TanStack/db/blob/main/packages/db/tests/comparison.property.test.ts), [deep equality](https://github.com/TanStack/db/blob/main/packages/db/tests/utils.property.test.ts), [cursor](https://github.com/TanStack/db/blob/main/packages/db/tests/cursor.property.test.ts), [indexes](https://github.com/TanStack/db/blob/main/packages/db/tests/index-update.property.test.ts), [BTree Map](https://github.com/TanStack/db/blob/main/packages/db/tests/btree-map-oracle.test.ts), [query identity](https://github.com/TanStack/db/blob/main/packages/db/tests/query/identity-output-shape-oracle.test.ts), [LIKE semantics](https://github.com/TanStack/db/blob/main/packages/db/tests/query/compiler/evaluators.test.ts) | Independent flat values, graph topology, algebraic laws, Map/group/sort recomputation, expression denotation, custom comparator dispatch/reference identity/index compatibility, LIKE wildcard refinement, compiled output bags, declared value-pair agreement between D2 equality keys and IR equality operands, and Collection collation with exact public scan/index order and one-index reuse across repeated reads. The auto-index cells cross BasicIndex and BTreeIndex; scan cells omit indexes. Both paths cover lexical and numeric `en-US` locale defaults plus an explicit ordinary locale override of a numeric default. An unindexed six-row work cell bounds Collection collation resolution to at most two reads per ordered scan: one index-path check and one scan clause. Other locales and ordered histories are outside this owner. The identity pair grammar covers primitives, signed zero, NaN/invalid Date, valid Date/timestamp, binary/Buffer, Temporal, nested references, symbols, and functions; fixed and sampled pairs do not prove every JavaScript value or hash collision freedom. Ref proxies are expressions rather than literal values in this lane. The hash-values owner samples Set/object and Map/object type-marker distinctions in fixed and random campaigns with seed-and-path replay; collision freedom is not promised. The hash-identity owner checks `hash` and `equalHashValues` against one spec-level identity model: primitives with signed zero, NaN, and bigint; reference leaves (functions, registered handles, `File`, binary over 128 bytes); Date, binary, Temporal, and RegExp headers; array length and holes; Map and Set insertion order; and object keys in any order, for acyclic values and for cycles through arrays, Map values, Sets, and plain objects. Map/Set order sensitivity is pinned current behavior, not a promise. Arrays from another realm, RegExps with a `NaN` `lastIndex`, getters, proxies, other cycle carriers, and pairs that differ only in a back-edge target are outside its grammar; pinned cases keep equality's cross-realm length check and strict RegExp field comparison. `hash-work.test.ts` pins how visited values and array/RegExp headers count toward the structural work cap, and that equality copies a shared Map's entries once per distinct pair ([code-weight hash dispatch review](oracle-reviews/code-weight-hash-dispatch.md)). Custom string indexes do not optimize ordinary ranges, and custom cursors remain unsupported. The LIKE owner covers boolean string matching and bounded work, not nullish three-valued logic or a general Unicode collation contract. Unsupported composite cursors reject. The MultiSet consolidation owner checks keyed, single-type, and structural identity, signed zero and NaN in keyed and single-number data, the retained first record, zero-sum removal, and input record contents. It does not assert output order. Its keyed grammar includes cross-type key and value collisions, non-finite numeric keys, the `\|` delimiter, symbol and function identity, and a direct-object/object-tuple delimiter control from #1948. Eight named collision witnesses and both generated campaigns are RED on `c879ba6d` and GREEN with the repair ([#1948 review record](oracle-reviews/issue-1948-keyed-consolidation.md)); the [code-weight consolidation review](oracle-reviews/code-weight-multiset-consolidation.md) records the earlier exclusion. The keyed reference uses direct value/reference equality, including SameValueZero for numeric keys. The unkeyed fallback keeps its original key and value domains. The direct `MultiSet` owner does not prove which `@tanstack/db` query shapes produce primitive keyed values or mixed-type source keys; that needs a live-query production witness. The index owner enforces one invalid-comparator law across both BasicIndex and BTreeIndex: for each comparator, accepted numeric prefix (including the empty prefix), and probe operation (add, update, eq/range lookup, take, build), the operation throws exactly when it receives a `NaN` or non-number result and never when every comparison is valid. A `signed infinity` comparator is the valid control, and a custom collation `compare` supplied through `compareOptions` is checked the same way. Two BTree Map split histories check inserted-key reads, exact whole-range payloads/order, size, and extrema, proving split placement follows the insertion index without a post-mutation comparison. A rejected index add or remove leaves the index refining its accepted rows; update and build are not atomic. At the Collection boundary, both index types and both write paths (optimistic insert, sync commit) crash the collection: status becomes `error` and the next mutation throws, so a usable collection never holds rows its subscribers were not told about. Open: a sync source can still commit writes on a collection in `error` state, which stores unpublished rows; this is existing `markError` behavior and needs a lifecycle-owner witness. Comparator transitivity is outside this evidence. | +| WHERE predicate publication | [WHERE predicate publication oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts) | An independent Kleene evaluator judges `eq`/`not`/`and`/`or` predicates over strings, a normalization-prefixed string, booleans, numbers, `NaN`, a valid Date, `null`, a missing field, and virtual fields. Snapshot histories with pending optimistic inserts compare a live query, direct subscribers with and without initial state, and `currentStateAsChanges`; change histories apply multi-key sync transactions, including reinsertion of a key deleted earlier, and compare every consumer's key set after each commit, on scan and `BasicIndex` paths. Peer subscribers share one field with different literals, plus one on a virtual field, so every published change must reach exactly the subscribers whose predicate it can satisfy; routing mutants that ignored the previous value, ignored the field, delivered a change twice, or routed while stale published rows awaited reconciliation fail here. Fixed witnesses cover a layout-only publication reaching a filtered subscriber as one empty batch, a filtered subscriber's empty Collection-readiness batch, retraction of a vanished row after eager cleanup and restart, and the new row plus exact subscriber update payload when a same-key row stays TRUE. A stale-live-value mutant survives the generated membership checks but fails the payload witness. A focused [property-visibility test](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-prefilter-property-visibility.test.ts) compares the public unindexed snapshot with the enriched-row contract for inherited, non-enumerable, enumerable-own, and nested getter paths; the pre-fix stored-row shortcut failed three of its four controls. Other property-descriptor and stateful-getter histories remain outside those fixed controls. Hostile mutants for FALSE-for-UNKNOWN `eq`, `or` or number-literal subscription prefilters, a prefilter that ignores the previous value, a skipped readiness batch, a skip while stale published rows await reconciliation, and an unindexed snapshot scan that reads only synced rows while an optimistic insert or delete changes visibility passed the prior `@tanstack/db` suite and fail here. Change routing withholds a dropped row from an `eq` subscription before its sent key could be recorded, so a mutant that records dropped rows as sent is equivalent for routed predicates in this oracle. A focused `collection-subscription.test.ts` witness checks that a dropped insert or update, alone or beside a matching change, cannot advance a limited subscription's page offset under a routed `eq` and an unrouted `or` predicate; the unrouted cases beside a matching change kill that mutant, and the unrouted `alone` case does not. A loose-equality prefilter is an equivalent mutant: it only skips less. Skipping during truncate replay also survives; the replay's baseline diff re-derives the same retraction, so the guard keeps the prior dataflow without its own witness. Generated cleanup and restart histories for filtered subscribers remain open for the lifecycle publication owner. No-op updates, other comparison operators, Temporal and binary operands, joins, ordering, optimistic updates, and truncate are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. | +| Joined result keys | [Joined result key oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/join-result-key-oracle.property.test.ts) | A nested-loop model recomputes the pair set of an inner, left, or full join over source keys drawn from plain, comma-bearing, bracket-bearing, and quoted strings, numbers beside the strings that print the same, both infinities, and `NaN`. After preload and each synced group change, the published rows must equal the model's pairs and the result key count must equal the row count. The comma-joined key encoding fails its two pinned histories and both campaigns; plain `JSON.stringify`, which prints infinities as the missing-side `null`, fails the pinned infinity history and both campaigns. A pinned history in which a `NaN`-keyed pair leaves and re-forms fails when the join index compares source-key prefixes with `===`. Joins over subqueries, more than two sources, custom `getKey`, and optimistic mutations are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. | +| D2 Index storage | [Index refinement oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/index-refinement-oracle.property.test.ts) | A plain-`Map` model sums multiplicities under a type-and-value identity in which `NaN` equals `NaN` and `-0` equals `0`. Histories of up to twelve additions to two keys mix prefixed arrays with `NaN`, `0`, `-0`, `1`, `'1'`, `1n`, and `'a'` prefixes and unprefixed values, with cancellation and reappearance. After each addition, `get` and `has` must equal the model. Comparing prefixes with `===`, which disagrees with the `Map` that holds them, fails three pinned histories and both campaigns; the `-0` control passes under both. Compaction, presence tracking, and structural payloads are outside this owner; the join operator tests and incrementalization law own operator behavior. | +| Index suggestions | [collection-size suggestion oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/index-suggestion-oracle.test.ts) | A public filtered live-query Collection checks manual/default, eager, threshold, matching-index, and unrelated-index cases against the documented collection-size suggestion policy. The original manual-silence/eager-noise implementation failed two expected observations. The owner does not judge repeated-query warning volume, slow-query timing, query work, production-build suppression, or every query shape. It runs in `@tanstack/db`'s `test:oracles` campaign. | +| Minified DB public API | [consumer bundle check](https://github.com/TanStack/db/blob/main/scripts/test-minified-db.mjs) | CI builds `@tanstack/db`, then bundles its built `dist` entry with db-ivm inlined through esbuild `minify: true`. The build renames TypeScript-private members from `packages/db/mangle-cache.json`, so this lane runs against the renamed output that consumers install. It rejects a `dist` that is older than `src`, the cache, or the package's build configuration. `pnpm --filter @tanstack/db test:dist` also runs five db test files against the built `dist`, chosen for coverage of the modules with the most renamed members. `pnpm check:mangle` fails when a cached name is used other than as a private member in db src, is read by another package, appears as a string, or has a short name that collides with a source identifier ([private-member mangling review](oracle-reviews/code-weight-private-member-mangling.md)). It checks every exported error class's current public `name`, the built-in `BasicIndex` resolver name in index metadata and its event, complete query rows (after removing the four documented virtual fields) against an independent array filter/sort/projection, and one live update. The `new.target.name`, public-member mangle, and extra-output-field calibration modes fail at the intended observations. This fixed slice does not replace the unminified generated oracles, exercise framework adapters, or establish stable names for custom index resolvers. | +| Boundary refinements | [cleanup/restart](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-cleanup-restart-oracle.test.ts), [issue #1891 async-cleanup review](oracle-reviews/issue-1891-async-cleanup.md), [issue #1891 cleanup-start follow-up](oracle-reviews/issue-1891-cleanup-start-followup.md), [metadata publication](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-metadata-publication-oracle.property.test.ts), [state retention](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-state-retention-oracle.property.test.ts), [acquisition cells](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-subscription-lifecycle-oracle.test.ts), [D2 source reconciliation](https://github.com/TanStack/db/blob/main/packages/db/tests/d2-source-reconciliation-oracle.property.test.ts), [top-K support windows](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/topk-support-window-oracle.test.ts), [nested Query work](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/includes-work-counter-oracle.test.ts) | Explicit lifecycle products, independent source maps and weighted relations, exact publication cuts, support/multiplicity, and value-plus-work observations. These refine the larger subsystem models; they do not replace them. | +| Small structures and test mechanics | [SortedMap](https://github.com/TanStack/db/blob/main/packages/db/tests/SortedMap.test.ts), [cleanup queue](https://github.com/TanStack/db/blob/main/packages/db/tests/cleanup-queue.property.test.ts), [live-query GC clock](https://github.com/TanStack/db/blob/main/packages/db/tests/collection-gc-clock.test.ts), [guarded replay](https://github.com/TanStack/db/blob/main/packages/db/tests/oracle-replay.test.ts) | Map/full-sort and elapsed appointment-list models with executed target/seed/path checks. SortedMap also checks deferred bulk writes, array-movement work, scan reuse, live iterators, runtime `undefined` keys, and public single-transaction and queued-transaction Collection sync paths. It does not settle the contract for comparator-observed values mutated after publication or every legal iterator/mutation interleaving. Wall-clock steps do not move due times, and a focused public witness checks source subscription release. Callback-reentrant scheduling, OS suspend/resume, environments without `performance.now()`, and fake/real Performance clock replacement while GC is pending remain outside this coverage. Tests must drain or reset pending GC before switching timer providers. | The hash-identity owner also runs every law in a second module copy whose initialization draws are all equal, so every type marker has the same hash @@ -521,14 +523,14 @@ The post-merge review added three missing domains to existing owners: The [PR #1907 review record](oracle-reviews/pr-1907-accepted-delete-ownership.md) preserves the exact RED/GREEN evidence and reviewed commits. -| Issue obligation | Implemented evidence | Limit | -| --- | --- | --- | -| Metamorphic laws | Includes cross-formulation/partition, D2 independent-key commutation, DBSP incremental/full recomputation, pagination provider/UI boundaries, optimistic snapshot stability | Equivalence premises are explicit; not arbitrary query rewrites. | -| Public observations | Reads, exact event payloads and reconstructed state, observer eligibility, downstream includes, lifecycle and ownership checks | Count/work budgets are separate from row truth and only pin established promises. | -| Meaningful async histories | Applied receipts, truncate/replay, cleanup/restart, pending optimistic work, held provider completion, leadership loss | Control real causes; unsupported SDK traces receive no coverage credit. | -| Checker sensitivity | Faulty output controls, missing/duplicate events, stale completion/ownership controls, forced collisions and pre-fix runtime witnesses | Setup failures and timeouts are recorded separately from assertion kills. | -| Executed reach and replay | Named manifest with guarded replay, finite boundary products, pinned examples, fixed/random lanes and explicit stress runs | Root `test:oracles` is a selected core/Query DB campaign, not all repository oracles. | -| Contract and scope records | Guide, companion case notes, this map, suite-local law/domain comments and architecture | This is an executable testing method, not a completeness proof or new product specification. | +| Issue obligation | Implemented evidence | Limit | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Metamorphic laws | Includes cross-formulation/partition, D2 independent-key commutation, DBSP incremental/full recomputation, pagination provider/UI boundaries, optimistic snapshot stability | Equivalence premises are explicit; not arbitrary query rewrites. | +| Public observations | Reads, exact event payloads and reconstructed state, observer eligibility, downstream includes, lifecycle and ownership checks | Count/work budgets are separate from row truth and only pin established promises. | +| Meaningful async histories | Applied receipts, truncate/replay, cleanup/restart, pending optimistic work, held provider completion, leadership loss | Control real causes; unsupported SDK traces receive no coverage credit. | +| Checker sensitivity | Faulty output controls, missing/duplicate events, stale completion/ownership controls, forced collisions and pre-fix runtime witnesses | Setup failures and timeouts are recorded separately from assertion kills. | +| Executed reach and replay | Named manifest with guarded replay, finite boundary products, pinned examples, fixed/random lanes and explicit stress runs | Root `test:oracles` is a selected core/Query DB campaign, not all repository oracles. | +| Contract and scope records | Guide, companion case notes, this map, suite-local law/domain comments and architecture | This is an executable testing method, not a completeness proof or new product specification. | ## Running and replaying @@ -593,82 +595,82 @@ regressions. Completion requires an executable owner, a production-path witness, a hostile wrong-answer control, and an explicit statement of remaining limits. - [x] **Real-provider conformance fixtures.** Frozen 15.2.7 React Native and - Node receipts cover the supported peer version; 18.2.1 React Native, Node, - and browser receipts cover the known forward shapes. Exact-row checks and a - row-dropping hostile control prove the shim accepts those envelopes without - mutating them. Owner: - `packages/react-native-db-sqlite-persistence/tests/fixtures/op-sqlite-provider-results.ts` - and `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts`. - Native device/host execution remains a separate runtime receipt. + Node receipts cover the supported peer version; 18.2.1 React Native, Node, + and browser receipts cover the known forward shapes. Exact-row checks and a + row-dropping hostile control prove the shim accepts those envelopes without + mutating them. Owner: + `packages/react-native-db-sqlite-persistence/tests/fixtures/op-sqlite-provider-results.ts` + and `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts`. + Native device/host execution remains a separate runtime receipt. - [x] **Minimal ambiguity and name invariance.** Generate one-field and - otherwise minimally distinguishable results. Renaming a selected column to a - structural-looking alias must not turn a data row into a write envelope. - Owner: `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts`. + otherwise minimally distinguishable results. Renaming a selected column to a + structural-looking alias must not turn a data row into a write envelope. + Owner: `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts`. - [x] **Carrier coexistence and representation symmetry.** Cross `rows`, - `rawRows`, `columnNames`, and supported result containers, including legal - coexistence. Equivalent array and object forms must agree on rows or on the - documented rejection. Owner: - `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts` - and the shared SQLite driver contract. + `rawRows`, `columnNames`, and supported result containers, including legal + coexistence. Equivalent array and object forms must agree on rows or on the + documented rejection. Owner: + `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts` + and the shared SQLite driver contract. - [x] **Shared-handle transaction admission.** Concurrent driver wrappers for - one provider database handle serialize root transactions. Nested work must - use the transaction driver supplied to the callback on hosts without async - context propagation. Owners: - `packages/node-db-sqlite-persistence/tests/node-driver.test.ts` and - `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts`. + one provider database handle serialize root transactions. Nested work must + use the transaction driver supplied to the callback on hosts without async + context propagation. Owners: + `packages/node-db-sqlite-persistence/tests/node-driver.test.ts` and + `packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts`. - [x] **Transitions at every relevant await.** Hold each coordination boundary, - then change leadership, remote-subset ownership, abort state, cleanup, or - restart generation before release. Owners: browser/electron coordinator, - persisted-history, and collection cleanup/restart oracles. + then change leadership, remote-subset ownership, abort state, cleanup, or + restart generation before release. Owners: browser/electron coordinator, + persisted-history, and collection cleanup/restart oracles. - [x] **Local-versus-transport refinement.** Compare local-leader and transported - remote-subset behavior for immutable values. Separately prove that local - `signal` and `subscription` references survive delivery and reach matching - unload cleanup. Owners: browser/electron coordinator and persisted-history - suites. + remote-subset behavior for immutable values. Separately prove that local + `signal` and `subscription` references survive delivery and reach matching + unload cleanup. Owners: browser/electron coordinator and persisted-history + suites. - [x] **Partial-construction cleanup.** Fail database, driver, worker, and - subscription construction after each acquired resource. Preserve the primary - failure while proving all acquired resources are released exactly once. - Owners: OP-SQLite driver-contract construction and OPFS page/worker lifecycle - suites. + subscription construction after each acquired resource. Preserve the primary + failure while proving all acquired resources are released exactly once. + Owners: OP-SQLite driver-contract construction and OPFS page/worker lifecycle + suites. - [x] **On-demand persistence after evidence changes.** Cross baseline versus - on-demand hydration with consistent, unknown, and incompatible key-set - evidence. A baseline certification failure must not silently erase valid - on-demand rows. `loadSubset` checks baseline visibility and on-demand rows; - the sync-absent `forceReloadSubset` route crosses the same startup evidence - states but records baseline visibility as unobserved because that API does - not expose baseline hydration. Owner: - `packages/db-sqlite-persistence-core/tests/persisted.test.ts`. + on-demand hydration with consistent, unknown, and incompatible key-set + evidence. A baseline certification failure must not silently erase valid + on-demand rows. `loadSubset` checks baseline visibility and on-demand rows; + the sync-absent `forceReloadSubset` route crosses the same startup evidence + states but records baseline visibility as unobserved because that API does + not expose baseline hydration. Owner: + `packages/db-sqlite-persistence-core/tests/persisted.test.ts`. - [x] **Deterministic value-and-work laws.** Pair row correctness with stable - statement, scan, trigger, or queue-cardinality observations where the - subsystem promises bounded work. Owners: SQLite resume snapshots, Electric - acquisition work, shared-driver scheduling suites, and the Node - same-database-handle admission law. + statement, scan, trigger, or queue-cardinality observations where the + subsystem promises bounded work. Owners: SQLite resume snapshots, Electric + acquisition work, shared-driver scheduling suites, and the Node + same-database-handle admission law. - [x] **Startup generation interleavings.** Hold persisted startup between its - metadata and hydration snapshots, then cross no write, a managed mutation, - and hostile raw loss. Compare public rows with the atomic durable snapshot, - and require mutation persistence to wait until the prior stream position is - known. Owners: SQLite resume snapshots and Electric resume snapshot races. + metadata and hydration snapshots, then cross no write, a managed mutation, + and hostile raw loss. Compare public rows with the atomic durable snapshot, + and require mutation persistence to wait until the prior stream position is + known. Owners: SQLite resume snapshots and Electric resume snapshot races. - [x] **Schema-generation fences on cached adapters.** Cross a newer-schema - reset with snapshot, row, metadata, delta, position, and index-lifecycle - operations from the cached older adapter. Every stale operation rejects; - the current adapter remains readable and its index registry remains intact. - Owners: SQLite resume snapshots plus browser/electron coordinator routing. + reset with snapshot, row, metadata, delta, position, and index-lifecycle + operations from the cached older adapter. Every stale operation rejects; + the current adapter remains readable and its index registry remains intact. + Owners: SQLite resume snapshots plus browser/electron coordinator routing. - [x] **Explicit omission records.** Add a short `Known omissions` section to - each primary executable owner touched above and keep this map synchronized as - laws land. An omission record narrows evidence; it does not waive a product - obligation. + each primary executable owner touched above and keep this map synchronized as + laws land. An omission record narrows evidence; it does not waive a product + obligation. - [ ] **Atomic active-subset full reload.** Load collection metadata and every - active subset from one adapter generation, including filtered and paginated - on-demand subsets. A sound implementation needs an atomic multi-subset API; - loading all rows or accepting a metadata/row torn pair is not equivalent. - RED evidence and the deferred executable placeholder live in - `packages/db-sqlite-persistence-core/tests/persisted.test.ts` under R5-007. + active subset from one adapter generation, including filtered and paginated + on-demand subsets. A sound implementation needs an atomic multi-subset API; + loading all rows or accepting a metadata/row torn pair is not equivalent. + RED evidence and the deferred executable placeholder live in + `packages/db-sqlite-persistence-core/tests/persisted.test.ts` under R5-007. - [x] **Joined aggregate and `singleResult` subquery histories.** - `packages/db/tests/query/subquery-user-value-oracle.test.ts` checks flat and - nested aggregate source changes and joined `findOne()` QueryRefs at initial - and later public checkpoints. The bounded histories and remaining forms are - recorded in the QueryRef owner row above. + `packages/db/tests/query/subquery-user-value-oracle.test.ts` checks flat and + nested aggregate source changes and joined `findOne()` QueryRefs at initial + and later public checkpoints. The bounded histories and remaining forms are + recorded in the QueryRef owner row above. ## Persisted readiness and network-first initial rendering diff --git a/docs/contributing/oracle-reviews/issue-1975-scope-identity.md b/docs/contributing/oracle-reviews/issue-1975-scope-identity.md new file mode 100644 index 0000000000..5818ba196c --- /dev/null +++ b/docs/contributing/oracle-reviews/issue-1975-scope-identity.md @@ -0,0 +1,125 @@ +# Issue #1975: alias scope identity + +Reviewed head: `18abceee4` (`origin/main`) plus the working-tree repair on +branch `red/issue-1975-materialize-alias`. Owner: +`packages/db/tests/query/includes-oracle.property.test.ts` +(`includes alpha-renaming across sibling scopes`), with grammar, model, +driver, and observation in +`packages/db/tests/query/includes-scope-identity-oracle.ts`. Alias +validation is owned by `packages/db/tests/query/validate-aliases.test.ts`. + +## Defect and cause + +A live query returned no rows when an include reused an alias from a joined +`from()` subquery that received a pushed-down predicate. The optimizer copied, +wrapped, or collapsed `CollectionRef` objects with `new CollectionRef(...)`, +which mints a fresh `SourceId`. Before #1877 the compiler compiled the user's +original subquery and discarded the optimized copy, so the orphaned IDs were +never read. #1877 compiles the optimized copy when it differs. The compiler +then missed every orphaned `SourceId` and fell back to alias text. +`bindSourceInputs` had written every scope's input under its alias, so the +lookup returned a sibling scope's source with the same name. + +## Repair + +- `optimizer.ts`: the four copy, wrap, and collapse sites reuse the existing + `CollectionRef` (lines near 500, 848, 935, and 945). +- `compiler/index.ts` and `compiler/joins.ts`: `bindSourceInputs` maps + caller-supplied alias keys to `SourceId`s once and no longer publishes + alias keys; both source lookups read `allInputs[sourceId]` only. A lost + identity now raises `CollectionInputNotFoundError`. +- `compiler/index.ts` `validateQueryStructure`: no scope inside an include + can reuse an alias its ancestors can see, including a subquery alias. This + covers the include itself, its `unionAll()` branches, and its `from()` and + join subqueries, which can read the parent row through their callbacks. + The builder previously accepted these namings and returned wrong rows. + Top-level `from()` subqueries see no ancestor, so they may still reuse names. +- `compiler/index.ts` `validateQueryStructure`: one query cannot give two of + its sources the same alias. Two joins with one alias were accepted before. + +## Bug-class boundary + +- **Contract:** ARCHITECTURE.md normative law 1 and §Identity: renaming an + accepted alias cannot change an explicitly projected result, and a plan + rewrite preserves `SourceId`. +- **Histories:** the owner grammar (three topologies; zero to two joins; + LEFT or INNER; zero to two predicates in separate or combined form; plain, + ordered-and-limited, or DISTINCT bodies; include form, source, and filter; + outer spread or fields; eager or on-demand finite providers; up to four + source writes). +- **Production path:** `createLiveQueryCollection` through optimizer, + compiler, and live builder. +- **Observations:** complete public rows after preload and after each write, + compared with recomputation and across namings; per-Collection + `loadSubset` WHERE clauses for on-demand sources. + +## Evidence + +| Check | Result | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Original reproduction | Issue query returns `[]` on `18abceee4`, `[1]` with the repair. Bisected first bad commit: `40a5aea56` (#1877). | +| Pinned witnesses | All 13 behavioral witnesses (issue rows plus six topologies in eager and on-demand modes) fail on `18abceee4` and pass with the repair. | +| Campaigns | Fixed seed `1715` and the random campaign (80 runs each) fail on `18abceee4` and pass with the repair. | +| Optimizer site mutants | Reverting site 500, 848, 935, or 945 alone fails the owner (4, 13, 13, and 13 tests). The pure-wrapper witness is the only witness that reaches site 500. | +| Identity-only binding | With it, each optimizer site mutant also fails the existing suite with `CollectionInputNotFoundError` (3, 53, 146, and 59 tests) instead of returning wrong rows. With the optimizer repair in place it is behavior-equivalent; its value is converting a future identity loss into an error. | +| Include shadowing | Four rejection witnesses (include, nested include, include `unionAll()` branch, include `from()` subquery) fail on `18abceee4` and pass with the repair; a sibling-reuse control passes on both. | +| Generated rejection | One in four scenarios draws any naming; illegal ones must be rejected, and shadowing ones with `DuplicateAliasInSubqueryError`. At a 10x budget this check found the duplicate-join acceptance, and it fails when include `from()` visibility or the same-scope check is reverted. Reverting union-branch visibility is caught only by its pinned `validate-aliases.test.ts` witness, because the include level already checks branch aliases. | +| Model calibration | Planted model faults (ignore child filter, LEFT as INNER, drop join duplicates) fail only at "canonical naming against recomputation". | +| Request calibration | An alias-dependent WHERE routing mutant fails only at "loadSubset requests per Collection". | +| Ablation | Restricting the earlier grammar to all-distinct names made both campaigns pass on the unrepaired code; the failure needs cross-scope reuse. | +| Enumeration (earlier grammar) | Failures needed a joined subquery with a pushed source predicate and an include reusing the subquery source or join alias. Outer-alias collisions alone never failed. | +| Full suite | `pnpm --filter @tanstack/db test` passes. | + +## Guide conformance (ORC-001 to ORC-014) + +- **ORC-001:** Law, authority, and limits are in the owner's opening comment. +- **ORC-002:** The recomputation model reads plain arrays and never consults + aliases, plans, or `SourceId`s. +- **ORC-003:** The contract and campaigns are in the owner. The grammar, + model, driver, and observation are in named sections of the companion. +- **ORC-004:** Reconstruction: every pinned witness is a grammar member. + Ablation and enumeration are recorded above. Range: IDs 1–4, three parts, + four refs and notes, and a three-name pool chosen for frequent collisions. + Exclusion: the legality test rejects shadowing, same-scope reuse, and + repeated `unionAll()` branch names. +- **ORC-005:** Real live-query Collections are compared at named checkpoints. + Failure messages carry the checkpoint, the write, and the naming. +- **ORC-006:** Site mutants, a hostile routing mutant, and planted model + faults are each rejected at the intended checkpoint. +- **ORC-007:** `generatedCampaigns` runs the same property with a fixed seed + and without one. Replay with `TANSTACK_DB_ORACLE_PROPERTY=includes.scoped-alpha-renaming` + plus `TANSTACK_DB_ORACLE_SEED` and `TANSTACK_DB_ORACLE_PATH`. The property + is registered in `oracle-config.ts` and `oracle-replay-manifest.ts`. +- **ORC-008:** Not applicable: the model is a stateless recomputation. +- **ORC-009:** The companion's opening comment maps slots, roles, aliases, and + `SourceId`. +- **ORC-010:** Cleanup failures join the primary failure in an + `AggregateError` whose `cause` is the violated law. +- **ORC-011:** Shared-fault hypothesis: every naming of one shape could be + wrong in the same way. The recomputation model is the second formulation + that distinguishes it. +- **ORC-012:** This record. +- **ORC-013:** The pinned wrapper witness distinguishes "reuse references + where the optimizer copies" from "reuse references except during + collapse". +- **ORC-014:** The on-demand provider is controlled. Its premise is a + `loadSubset` WHERE clause naming Collection fields. Real-provider + reception of that premise belongs to the load-subset owners. + +## Open in-scope cells + +These legal forms are in the bug class but outside the owner's grammar. Probes +during review passed on both revisions, so none is a known counterexample, but +none is protected by this owner: + +- nested includes, and includes inside a `from()` subquery; +- join subqueries outside the wrapper topology, and joined subqueries with + `groupBy` or `having`; +- outer RIGHT and FULL joins; +- publication events, observer timing, and unload under reused names; +- ordered windows and lazy join loading, whose lazy-target lookup reads + `aliasRemapping`, a map merged by alias across include scopes; +- temporal demand, cancellation, and progressive loading. + +The identity-only binding makes a lost `SourceId` raise an error in any of +these forms. That is the current protection against silent wrong rows there. diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index fdad2c8f5f..8a485c000a 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -25,6 +25,7 @@ import { FnSelectWithGroupByError, HavingRequiresGroupByError, LimitOffsetRequireOrderByError, + QueryCompilationError, UnsupportedFnSelectResultError, UnsupportedFromTypeError, } from '../../errors.js' @@ -1244,11 +1245,12 @@ function bindSourceInputs( sources: Array, inputs: Record, ): void { + // Callers may key inputs by alias. Bind them to source identities once; + // compilation then reads inputs by SourceId only, so a source whose + // identity was lost fails instead of reading a same-named source. for (const source of sources) { const input = inputs[source.sourceId] ?? inputs[source.alias] - if (!input) continue - inputs[source.sourceId] = input - inputs[source.alias] = input + if (input) inputs[source.sourceId] = input } } @@ -1339,7 +1341,25 @@ function collectDirectCollectionAliases(query: QueryIR): Set { function validateQueryStructure( query: QueryIR, parentCollectionAliases: Set = new Set(), + visibleAliases: Set = new Set(), ): void { + // One scope cannot name two sources alike. + const levelAliases = getAllSources(query).map((source) => source.alias) + for (const [index, alias] of levelAliases.entries()) { + if (levelAliases.indexOf(alias) !== index) { + throw new QueryCompilationError( + `Query uses alias "${alias}" more than once. Give each source in one query a distinct alias.`, + ) + } + } + + // A scope cannot shadow an alias that its ancestors can see. + for (const alias of collectScopeAliases(query)) { + if (visibleAliases.has(alias)) { + throw new DuplicateAliasInSubqueryError(alias, [...visibleAliases]) + } + } + // Collect direct collection aliases from this query level const currentLevelAliases = collectDirectCollectionAliases(query) @@ -1362,12 +1382,12 @@ function validateQueryStructure( // Recursively validate FROM subqueries if (query.from.type === `unionAll`) { for (const branch of query.from.queries) { - validateQueryStructure(branch, combinedAliases) + validateQueryStructure(branch, combinedAliases, visibleAliases) } } else { for (const source of getFromSources(query.from)) { if (source.type === `queryRef`) { - validateQueryStructure(source.query, combinedAliases) + validateQueryStructure(source.query, combinedAliases, visibleAliases) } } } @@ -1376,18 +1396,40 @@ function validateQueryStructure( if (query.join) { for (const joinClause of query.join) { if (joinClause.from.type === `queryRef`) { - validateQueryStructure(joinClause.from.query, combinedAliases) + validateQueryStructure( + joinClause.from.query, + combinedAliases, + visibleAliases, + ) } } } + // An include sees every alias of its ancestors, including subquery + // aliases, so it cannot shadow any of them. if (query.select) { + const scopeAliases = new Set([ + ...visibleAliases, + ...collectScopeAliases(query), + ]) for (const { subquery } of extractIncludesFromSelect(query.select)) { - validateQueryStructure(subquery.query, combinedAliases) + validateQueryStructure(subquery.query, combinedAliases, scopeAliases) } } } +// unionAll() branches belong to the scope of the query that unions them. +function collectScopeAliases(query: QueryIR): Array { + const branchAliases = + query.from.type === `unionAll` + ? query.from.queries.flatMap(collectScopeAliases) + : [] + return [ + ...branchAliases, + ...getAllSources(query).map((source) => source.alias), + ] +} + /** * Processes the FROM clause, handling direct collection references and subqueries. * Populates `aliasToCollectionId` and `aliasRemapping` for per-alias subscription tracking. @@ -1747,7 +1789,7 @@ function processFrom( } { switch (from.type) { case `collectionRef`: { - const input = allInputs[from.sourceId] ?? allInputs[from.alias] + const input = allInputs[from.sourceId] if (!input) { throw new CollectionInputNotFoundError( from.alias, diff --git a/packages/db/src/query/compiler/joins.ts b/packages/db/src/query/compiler/joins.ts index fa8ce1808d..390807ccbf 100644 --- a/packages/db/src/query/compiler/joins.ts +++ b/packages/db/src/query/compiler/joins.ts @@ -613,7 +613,7 @@ function processJoinSource( ): { alias: string; input: KeyedStream; collectionId: string } { switch (from.type) { case `collectionRef`: { - const input = allInputs[from.sourceId] ?? allInputs[from.alias] + const input = allInputs[from.sourceId] if (!input) { throw new CollectionInputNotFoundError( from.alias, diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index a6ef6d9188..84076f0fa0 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -155,6 +155,13 @@ a namespaced row whose keys are the lexical aliases. Those observable keys are part of query identity. Alias text may otherwise remain as debug metadata without becoming source identity. +A plan rewrite must preserve each Collection reference's `SourceId`. The +optimizer reuses the reference when it copies, wraps, or collapses a source, +and compilation reads source inputs by `SourceId` only. A source whose identity +was lost raises `CollectionInputNotFoundError`; it never reads a same-named +source from another scope. Every alias in every `unionAll()` branch belongs to +one namespace, and the compiler rejects a repeated name. + A `CanonicalCorrelationKey` is the canonical tuple of every evaluated parent-dependent value that can affect the child plan. This includes values used by filters, joins, grouping, aggregates, ordering, projections, limits, @@ -1250,7 +1257,8 @@ create recursive Collection machinery. 1. **Alpha-renaming:** changing any accepted alias to another unused name cannot change an explicitly projected result. An implicit namespaced result keeps its aliases as public field names. Aliases must be unique within one lexical - scope and cannot shadow an ancestor alias. Sibling scopes may reuse aliases. + scope and cannot shadow an ancestor alias. Sibling scopes may reuse aliases, + except that all `unionAll()` branches share one alias namespace. 2. **Contribution conservation:** a public row exists exactly when its reduced supporting weight and collision policy produce one. 3. **Batch partition:** equivalent valid split and atomic deliveries converge. @@ -1323,26 +1331,27 @@ keep the meanings defined there. ## Executable contracts -| Contract | Test suite | -| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| State equivalence, route lifecycle, transition history, and batch partition | `packages/db/tests/query/includes-oracle.property.test.ts` | -| Joined multiplicity, alias identity, and null-key normalization | `packages/db/tests/query/includes-query-shape-oracle.test.ts` | -| Demand, cancellation, and progressive timing | `packages/db/tests/query/includes-temporal-oracle.test.ts` | -| Optimistic confirmation, rollback, and later reactivity | `packages/db/tests/query/includes-optimistic-oracle.property.test.ts` | -| Coherent layered publication | `packages/db/tests/query/includes-publication-oracle.test.ts` | -| Collection facades, event coherence, and route activation | `packages/db/tests/query/includes-collection-oracle.property.test.ts` | -| Correlated physical work | `packages/db/tests/query/includes-work-counter-oracle.test.ts` | -| Constructed and retained facades in a nested Collection tree | `packages/db/tests/query/includes-space-oracle.test.ts` | -| Route-context discovery and transport across recursive and join boundaries | `packages/db/tests/query/includes-context-transport-oracle.test.ts` | -| Functional projection input boundaries, timing, and output preservation | `packages/db/tests/query/includes-functional-projection-oracle.test.ts` | -| Functional input rejection and inline alternatives | `packages/db/tests/query/includes-functional-input-boundary.test.ts` | -| Public-container descriptors and reference-key matches across internal query stages | `packages/db/tests/query/public-container-copy.test.ts` | -| Cross-formulation equivalence and reference-sensitive route identity | `packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts` | -| Query-db ownership | `packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts` | -| Failed replay retention, peer isolation, and explicit consumer-only recovery | `packages/db/tests/query/replay-failure-boundary.test.ts` | -| Replay lease balance, reference-counted peers, and failed-start recovery | `packages/db/tests/replay-adapter-ownership.test.ts` | -| Reachable nested shape | `packages/query-db-collection/tests/includes-work-counter-oracle.test.ts` | -| Cleanup-start invalidation, settlement, and restart admission | `packages/db/tests/collection-cleanup-restart-oracle.test.ts` | +| Contract | Test suite | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| State equivalence, route lifecycle, transition history, and batch partition | `packages/db/tests/query/includes-oracle.property.test.ts` | +| Joined multiplicity, alias identity, and null-key normalization | `packages/db/tests/query/includes-query-shape-oracle.test.ts` | +| Alias reuse across sibling scopes and `SourceId` preservation | `packages/db/tests/query/includes-oracle.property.test.ts` (sibling scopes), `packages/db/tests/query/validate-aliases.test.ts` | +| Demand, cancellation, and progressive timing | `packages/db/tests/query/includes-temporal-oracle.test.ts` | +| Optimistic confirmation, rollback, and later reactivity | `packages/db/tests/query/includes-optimistic-oracle.property.test.ts` | +| Coherent layered publication | `packages/db/tests/query/includes-publication-oracle.test.ts` | +| Collection facades, event coherence, and route activation | `packages/db/tests/query/includes-collection-oracle.property.test.ts` | +| Correlated physical work | `packages/db/tests/query/includes-work-counter-oracle.test.ts` | +| Constructed and retained facades in a nested Collection tree | `packages/db/tests/query/includes-space-oracle.test.ts` | +| Route-context discovery and transport across recursive and join boundaries | `packages/db/tests/query/includes-context-transport-oracle.test.ts` | +| Functional projection input boundaries, timing, and output preservation | `packages/db/tests/query/includes-functional-projection-oracle.test.ts` | +| Functional input rejection and inline alternatives | `packages/db/tests/query/includes-functional-input-boundary.test.ts` | +| Public-container descriptors and reference-key matches across internal query stages | `packages/db/tests/query/public-container-copy.test.ts` | +| Cross-formulation equivalence and reference-sensitive route identity | `packages/db/tests/query/includes-cross-formulation-oracle.property.test.ts` | +| Query-db ownership | `packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts` | +| Failed replay retention, peer isolation, and explicit consumer-only recovery | `packages/db/tests/query/replay-failure-boundary.test.ts` | +| Replay lease balance, reference-counted peers, and failed-start recovery | `packages/db/tests/replay-adapter-ownership.test.ts` | +| Reachable nested shape | `packages/query-db-collection/tests/includes-work-counter-oracle.test.ts` | +| Cleanup-start invalidation, settlement, and restart admission | `packages/db/tests/collection-cleanup-restart-oracle.test.ts` | Each oracle identifies the first divergent checkpoint and compares either the whole result or one exact structural difference. Correlated-materialization diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index b7487533ed..1200d1cf7d 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -124,7 +124,6 @@ import { deepEquals } from '../utils.js' import { CannotCombineEmptyExpressionListError } from '../errors.js' import { containsAggregate } from './compiler/group-by.js' import { - CollectionRef as CollectionRefClass, Func, PropRef, QueryRef as QueryRefClass, @@ -135,7 +134,14 @@ import { getWhereExpression, isResidualWhere, } from './ir.js' -import type { BasicExpression, From, QueryIR, Select, Where } from './ir.js' +import type { + BasicExpression, + CollectionRef as CollectionRefClass, + From, + QueryIR, + Select, + Where, +} from './ir.js' /** * Represents a WHERE clause after source analysis @@ -497,7 +503,7 @@ function removeRedundantFromClause(from: From): From { // Return the inner query's FROM clause with this alias const innerFrom = removeRedundantFromClause(processedQuery.from) if (innerFrom.type === `collectionRef`) { - return new CollectionRefClass(innerFrom.collection, from.alias) + return innerFrom } else if (innerFrom.type === `queryRef`) { return new QueryRefClass(innerFrom.query, from.alias) } @@ -844,8 +850,9 @@ function deepCopyQuery(query: QueryIR): QueryIR { } function deepCopyFrom(from: From): From { + // Share the CollectionRef: its SourceId is the compiled input identity. if (from.type === `collectionRef`) { - return new CollectionRefClass(from.collection, from.alias) + return from } if (from.type === `queryRef`) { @@ -930,9 +937,10 @@ function optimizeFromWithTracking( const whereClause = singleSourceClauses.get(from.alias) if (!whereClause) { - // No optimization needed, but return a copy to maintain immutability + // No optimization needed. Keep the CollectionRef itself: its sourceId is + // the compiled input identity, and a copy would lose it. if (from.type === `collectionRef`) { - return new CollectionRefClass(from.collection, from.alias) + return from } // Must be queryRef due to type system return new QueryRefClass(deepCopyQuery(from.query), from.alias) @@ -942,7 +950,7 @@ function optimizeFromWithTracking( // Create a new subquery with the WHERE clause for the collection // This is always safe since we're creating a new subquery const subQuery: QueryIR = { - from: new CollectionRefClass(from.collection, from.alias), + from, where: [whereClause], } actuallyOptimized.add(from.alias) // Mark as successfully optimized diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index d59a85d33f..bfccf89b77 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -103,6 +103,7 @@ const staticOracleProperties = [ `includes-temporal.partial-values`, `includes-temporal.demand-scheduling`, `includes.alpha-renaming`, + `includes.scoped-alpha-renaming`, `includes.incremental-history`, `includes.nested-scalar-materialization`, `includes.optimistic-convergence`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index 11055ddac0..09384a35f5 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -149,7 +149,7 @@ const ownerGroups: ReadonlyArray = [ [ `db/tests/query/includes-oracle.property.test.ts`, `includes`, - `scenario-statistics incremental-history nested-scalar-materialization alpha-renaming optimistic-convergence`, + `scenario-statistics incremental-history nested-scalar-materialization alpha-renaming scoped-alpha-renaming optimistic-convergence`, ], [ `db/tests/query/includes-query-shape-oracle.test.ts`, diff --git a/packages/db/tests/query/includes-oracle.property.test.ts b/packages/db/tests/query/includes-oracle.property.test.ts index 8043297da6..94fcfa30d7 100644 --- a/packages/db/tests/query/includes-oracle.property.test.ts +++ b/packages/db/tests/query/includes-oracle.property.test.ts @@ -1,5 +1,6 @@ import { fc, test as fcTest } from '@fast-check/vitest' import { describe, expect } from 'vitest' +import { DuplicateAliasInSubqueryError } from '../../src/errors.js' import { concat, createLiveQueryCollection, @@ -12,12 +13,35 @@ import { flushPromises, withExpectedRejection } from '../utils.js' import { oraclePropertyOptions, oracleRuns } from '../oracle-config.js' import { runTrace } from '../trace-runner.js' import { createControlledCollection as createOracleControlledCollection } from './includes-oracle-helpers.js' +import { + applyScopedWrite, + canonicalScopedAliases, + classifyScopedNaming, + createModelState, + createScopedQuery, + createScopedSources, + normalizeScopedRows, + recomputeScopedRows, + scopedScenarioArbitrary, + sortedRequests, +} from './includes-scope-identity-oracle.js' import type { TraceCheckpoint, TraceDriver, TraceProjection, } from '../trace-runner.js' import type { OracleSyncChange as SyncChange } from './includes-oracle-helpers.js' +import type { + ScopedAliases, + ScopedInclude, + ScopedNaming, + ScopedPart, + ScopedRef, + ScopedResultRow, + ScopedScenario, + ScopedShape, + ScopedSubquery, +} from './includes-scope-identity-oracle.js' /** * # Does the incremental include graph equal full relationship recomputation? @@ -40,7 +64,8 @@ import type { OracleSyncChange as SyncChange } from './includes-oracle-helpers.j * the same logical source state. * 4. Nested scalar materialization follows reference changes and shared paths. * 5. Alpha-renaming, sibling order, and unrelated siblings do not change the - * relevant result. + * relevant result, including when sibling scopes such as a `from()` + * subquery and an include reuse alias names. * * These laws use a model graph, not one universal controller. The structural * node recomputes relationship trees. A separate scalar-reference node follows @@ -5563,3 +5588,385 @@ describe(`includes recompute oracle`, () => { ) }) }) + +/** + * # Does an alias name in one scope leak into another? + * + * Law: ARCHITECTURE.md normative law 1. Changing any accepted alias to another + * legal name cannot change an explicitly projected result. Aliases are lexical + * names; a `SourceId` is the plan's identity for one Collection reference + * (§Identity). A `from()` subquery, a `unionAll()` branch, a join subquery, + * and an include are sibling scopes when neither can see the other's names, + * so they may reuse names freely. + * + * Bug class: a plan rewrite loses a `SourceId`, or alias text otherwise + * decides which source's input, request, or remap is used. A sibling source + * with the same name then supplies the wrong rows. Issue #1975 reached it + * through optimizer predicate pushdown into a joined `from()` subquery. + * + * Grammar, model, driver, and observation live in + * `includes-scope-identity-oracle.ts`. The grammar crosses three topologies + * that place a rewritable source beside a same-named sibling source: + * `fromSubquery`, `unionBranch`, and a top-level pure `wrapper` beside a join + * subquery. The subquery varies joins (none, refs, refs then notes), LEFT or + * INNER refs joins, zero to two predicates in separate or combined form, a + * plain, ordered-and-limited, or DISTINCT body, and whole-row or field + * selection. The include varies its body (direct, joined, a `unionAll()` + * with an anchor join, or a `from()` subquery that reads the parent row), + * `toArray()` or `materialize()`, its source, an extra child predicate, and + * outer spread or field selection. Sources are + * eager or on-demand finite providers. Alias slots draw from a three-name + * pool, so cross-scope reuse is frequent. + * + * Legality follows the documented lexical rules: one scope keeps its names + * distinct, and no scope inside an include can reuse an alias its ancestors + * can see. One in four scenarios draws any naming. An illegal naming must be + * rejected when the query is created; a shadowing naming must be rejected + * with `DuplicateAliasInSubqueryError`. `validate-aliases.test.ts` pins the + * named shadowing cases. + * + * Checks at each checkpoint (after preload and after every source write): + * + * 1. The canonical all-distinct naming equals a plain recomputation of the + * current source rows. This judges the shape independently of aliases. + * 2. The generated naming equals the canonical naming. This is the law. + * 3. For on-demand sources, each Collection receives the same `loadSubset` + * WHERE clauses under both namings. Requests name Collection fields, not + * aliases, so they must match exactly as bags. + * + * Limits: the model covers this grammar only. Rows and members are compared + * as bags because no query here promises an order. Publication events, + * observer timing, unload, and nested includes are outside this owner; the + * includes publication and recomputation owners cover them for their shapes. + * The review record is + * `docs/contributing/oracle-reviews/issue-1975-scope-identity.md`. + */ +async function expectScopedNamingRejected( + scenario: ScopedScenario, + naming: Exclude, +) { + const sources = createScopedSources(scenario) + let created: ReturnType | undefined + try { + expect( + () => { + created = createScopedQuery(scenario.shape, scenario.aliases, sources) + }, + `${naming} naming ${JSON.stringify(scenario.aliases)} must be rejected`, + ).toThrow(naming === `shadowing` ? DuplicateAliasInSubqueryError : Error) + } finally { + await created?.cleanup() + await Promise.all( + Object.values(sources).map((source) => source.collection.cleanup()), + ) + } +} + +async function expectScopedAlphaRenamingHolds( + scenario: ScopedScenario, + expectedAfterPreload?: Array, +) { + const naming = classifyScopedNaming(scenario.shape, scenario.aliases) + if (naming !== `legal`) { + await expectScopedNamingRejected(scenario, naming) + return + } + const canonicalNames = canonicalScopedAliases(scenario.shape) + const canonicalSources = createScopedSources(scenario) + const renamedSources = createScopedSources(scenario) + const state = createModelState(scenario) + const canonical = createScopedQuery( + scenario.shape, + canonicalNames, + canonicalSources, + ) + const renamed = createScopedQuery( + scenario.shape, + scenario.aliases, + renamedSources, + ) + + const check = (checkpoint: string) => { + const label = `${checkpoint}, naming ${JSON.stringify(scenario.aliases)}` + expect( + normalizeScopedRows(canonical.toArray), + `${label}: canonical naming against recomputation`, + ).toEqual(normalizeScopedRows(recomputeScopedRows(scenario.shape, state))) + expect( + normalizeScopedRows(renamed.toArray), + `${label}: generated naming against canonical naming`, + ).toEqual(normalizeScopedRows(canonical.toArray)) + if (scenario.mode === `onDemand`) { + expect( + sortedRequests(renamedSources), + `${label}: loadSubset requests per Collection`, + ).toEqual(sortedRequests(canonicalSources)) + } + } + + let primary: unknown + try { + await Promise.all([canonical.preload(), renamed.preload()]) + await flushPromises() + if (expectedAfterPreload) { + expect(normalizeScopedRows(canonical.toArray)).toEqual( + normalizeScopedRows(expectedAfterPreload), + ) + } + check(`after preload`) + for (const [index, write] of scenario.writes.entries()) { + applyScopedWrite([canonicalSources, renamedSources], state, write) + await flushPromises() + check(`after write ${index} (${JSON.stringify(write)})`) + } + } catch (error) { + primary = error + } + + // Cleanup failures must not replace the violated law. + const cleanup = await Promise.allSettled([ + canonical.cleanup(), + renamed.cleanup(), + ...[canonicalSources, renamedSources].flatMap((sources) => + Object.values(sources).map((source) => source.collection.cleanup()), + ), + ]) + const cleanupErrors = cleanup.flatMap((result) => + result.status === `rejected` ? [result.reason] : [], + ) + if (primary !== undefined && cleanupErrors.length > 0) { + throw new AggregateError( + [primary, ...cleanupErrors], + `scoped alpha-renaming failed, and cleanup also failed`, + { cause: primary }, + ) + } + if (primary !== undefined) throw primary + if (cleanupErrors.length > 0) { + throw new AggregateError(cleanupErrors, `scoped oracle cleanup failed`) + } +} + +const scopedPartRows: Array = [ + { id: 1, active: true }, + { id: 2, active: true }, + { id: 3, active: false }, +] +const scopedRefRows: Array = [ + { id: 10, partId: 1, clientId: 1 }, + { id: 11, partId: 1, clientId: 2 }, + { id: 20, partId: 2, clientId: 2 }, + { id: 30, partId: 3, clientId: 1 }, +] +const scopedNoteRows: Array = [ + { id: 100, partId: 1, clientId: 1 }, + { id: 200, partId: 2, clientId: 1 }, +] + +const plainSubquery: ScopedSubquery = { + joins: `refs`, + refsJoin: `left`, + predicates: [`subActive`], + predicateForm: `separate`, + body: `plain`, + subSelect: `row`, +} +const refsInclude: ScopedInclude = { + body: `plain`, + form: `toArray`, + source: `refs`, + clientFilter: false, + outerSelect: `spread`, +} + +function pinnedScenario( + shape: ScopedShape, + aliases: ScopedAliases, +): ScopedScenario { + return { + shape, + mode: `eager`, + aliases, + parts: scopedPartRows, + refs: scopedRefRows, + notes: scopedNoteRows, + writes: [], + } +} + +/** + * Each pinned witness failed on the unrepaired optimizer and passes with the + * repair. Together they reach every optimizer site that once re-minted a + * SourceId; the review record maps each site to its killing witness. + */ +const pinnedScopeWitnesses: Array<[string, ScopedScenario]> = [ + [ + `an include reuses the join alias of a joined subquery`, + pinnedScenario( + { + topology: `fromSubquery`, + subquery: { + ...plainSubquery, + predicates: [`joinedClient`, `subActive`], + }, + include: { ...refsInclude, form: `materialize` }, + }, + { outer: `part`, sub: `part`, joined: `ref`, noted: `n`, include: `ref` }, + ), + ], + [ + `an include reuses the second join alias`, + pinnedScenario( + { + topology: `fromSubquery`, + subquery: { ...plainSubquery, joins: `refsThenNotes` }, + include: refsInclude, + }, + { outer: `o`, sub: `s`, joined: `j`, noted: `k`, include: `k` }, + ), + ], + [ + `an include reuses the source alias of an ordered, limited subquery`, + pinnedScenario( + { + topology: `fromSubquery`, + subquery: { ...plainSubquery, body: `orderedLimit` }, + include: refsInclude, + }, + { outer: `o`, sub: `p`, joined: `j`, noted: `n`, include: `p` }, + ), + ], + [ + `an include reuses the source alias of a DISTINCT subquery`, + pinnedScenario( + { + topology: `fromSubquery`, + subquery: { ...plainSubquery, body: `distinct`, subSelect: `fields` }, + include: refsInclude, + }, + { outer: `o`, sub: `p`, joined: `j`, noted: `n`, include: `p` }, + ), + ], + [ + `an include reuses the source alias of a joined union branch`, + pinnedScenario( + { + topology: `unionBranch`, + subquery: { ...plainSubquery, subSelect: `fields` }, + include: refsInclude, + }, + { + outer: `o`, + sub: `x`, + joined: `j`, + noted: `n`, + include: `x`, + inactive: `y`, + }, + ), + ], + [ + `a join subquery reuses the alias of a collapsed pure wrapper`, + pinnedScenario( + { topology: `wrapper`, wrapperJoin: `inner` }, + { wrapped: `x`, joinSub: `k`, joinSource: `x` }, + ), + ], +] + +describe(`includes alpha-renaming across sibling scopes`, () => { + fcTest( + `an include that reuses a joined subquery alias keeps the reported rows`, + // Issue #1975: only part 1 has a ref for client 1; parts 1 and 2 are + // active. + () => + expectScopedAlphaRenamingHolds( + { + ...pinnedScopeWitnesses[0]![1], + parts: [ + { id: 1, active: true }, + { id: 2, active: true }, + ], + refs: [ + { id: 10, partId: 1, clientId: 1 }, + { id: 20, partId: 2, clientId: 2 }, + ], + notes: [], + }, + [{ id: 1, active: true, members: [{ id: 10, clientId: 1 }] }], + ), + ) + + for (const [name, scenario] of pinnedScopeWitnesses) { + for (const mode of [`eager`, `onDemand`] as const) { + fcTest(`${name} [${mode}]`, () => + expectScopedAlphaRenamingHolds({ ...scenario, mode }), + ) + } + } + + fcTest(`legality rejects shadowing and same-scope reuse`, () => { + const shape: ScopedShape = { + topology: `fromSubquery`, + subquery: { ...plainSubquery, joins: `refsThenNotes` }, + include: refsInclude, + } + const legal = { + outer: `a`, + sub: `a`, + joined: `b`, + noted: `c`, + include: `b`, + } + expect(classifyScopedNaming(shape, legal)).toBe(`legal`) + expect(classifyScopedNaming(shape, { ...legal, include: `a` })).toBe( + `shadowing`, + ) + expect(classifyScopedNaming(shape, { ...legal, noted: `b` })).toBe( + `sameScope`, + ) + expect( + classifyScopedNaming( + { ...shape, topology: `unionBranch` }, + { ...legal, inactive: `c` }, + ), + ).toBe(`sameScope`) + expect( + classifyScopedNaming( + { topology: `wrapper`, wrapperJoin: `left` }, + { wrapped: `x`, joinSub: `x`, joinSource: `y` }, + ), + ).toBe(`sameScope`) + const nestedShape: ScopedShape = { + ...shape, + include: { ...refsInclude, body: `nestedFrom` }, + } + expect( + classifyScopedNaming(nestedShape, { + ...legal, + include: `c`, + includeOuter: `c`, + }), + ).toBe(`legal`) + expect( + classifyScopedNaming(nestedShape, { + ...legal, + include: `a`, + includeOuter: `c`, + }), + ).toBe(`shadowing`) + }) + + for (const { label, options } of generatedCampaigns( + 80, + `includes.scoped-alpha-renaming`, + 1715, + )) { + fcTest.prop([scopedScenarioArbitrary], options)( + `is unchanged when sibling scopes reuse alias names [${label}]`, + (scenario) => expectScopedAlphaRenamingHolds(scenario), + // Each run builds four live queries; scale with the run budget. + Math.max(5_000, oracleRuns(80) * 50), + ) + } +}) diff --git a/packages/db/tests/query/includes-scope-identity-oracle.ts b/packages/db/tests/query/includes-scope-identity-oracle.ts new file mode 100644 index 0000000000..588892f8d8 --- /dev/null +++ b/packages/db/tests/query/includes-scope-identity-oracle.ts @@ -0,0 +1,772 @@ +import { fc } from '@fast-check/vitest' +import { createCollection } from '../../src/collection/index.js' +import { createFilterFunctionFromExpression } from '../../src/collection/change-events.js' +import { + and, + createLiveQueryCollection, + eq, + materialize, + not, + toArray, +} from '../../src/query/index.js' +import { mockSyncCollectionOptions } from '../utils.js' +import type { Collection } from '../../src/collection/index.js' +import type { LoadSubsetOptions } from '../../src/types.js' + +/** + * Companion module for the `includes alpha-renaming across sibling scopes` + * owner in `includes-oracle.property.test.ts`. That file states the law and + * runs the campaigns. This module keeps the other four responsibilities in + * separate sections: grammar, reference model, production driver, and + * observation. + * + * Vocabulary. A query alias is lexical text. A `SourceId` (ARCHITECTURE.md + * §Identity) is the opaque identity of one Collection reference in the plan. + * The model has neither: it names sources by role (`part`, `joined`, `note`, + * `include`) and computes rows from plain arrays. A slot is a model-only name + * for one alias position in a query shape. + */ + +export type ScopedPart = { id: number; active: boolean } +export type ScopedRef = { id: number; partId: number; clientId: number } + +// ## Grammar + +/** + * Three topologies place a rewritable source and a same-named source in + * sibling scopes. Each one reached a lost-SourceId failure on the + * unrepaired optimizer: + * + * - `fromSubquery`: an outer `from()` subquery and a correlated include. + * - `unionBranch`: the same subquery as one `unionAll()` branch, beside an + * inactive-parts branch, under an outer `from()` and an include. + * - `wrapper`: a top-level pure wrapper `from({ x: from({ x: parts }) })` + * beside a join subquery. Predicate pushdown would restructure the wrapper + * first, so this topology has no WHERE clause; it reaches the redundant + * subquery collapse. + */ +export type ScopedTopology = `fromSubquery` | `unionBranch` | `wrapper` + +export type ScopedSubquery = { + /** Joins of the subquery source: none, refs, or refs then notes. */ + joins: `none` | `refs` | `refsThenNotes` + refsJoin: `left` | `inner` + predicates: Array<`subActive` | `joinedClient`> + predicateForm: `separate` | `and` + /** Clauses after WHERE that change optimizer safety and compiled shape. */ + body: `plain` | `orderedLimit` | `distinct` + subSelect: `row` | `fields` +} + +export type ScopedInclude = { + /** + * How the include reads its source. Every body selects the same members: + * `plain` filters the source directly; `joined` inner-joins each member to + * its part; `union` splits the source into two `unionAll()` branches and + * correlates through an anchor join; `nestedFrom` reads a `from()` + * subquery whose callback reads the parent row. + */ + body: `plain` | `joined` | `union` | `nestedFrom` + form: `toArray` | `materialize` + source: `refs` | `notes` + clientFilter: boolean + outerSelect: `spread` | `fields` +} + +export type ScopedShape = + | { + topology: `fromSubquery` | `unionBranch` + subquery: ScopedSubquery + include: ScopedInclude + } + | { topology: `wrapper`; wrapperJoin: `left` | `inner` } + +/** + * Alias slots. `fromSubquery` and `unionBranch` use `outer`, `sub`, + * `joined`, `noted`, `include`, (`unionBranch` only) `inactive`, and the + * include body's extra slots: `includeJoin`, `includeOther` and + * `includeAnchor`, or `includeOuter`. `wrapper` uses `wrapped`, `joinSub`, + * and `joinSource`. + */ +export type ScopedAliases = Record + +export type ScopedSourceMode = `eager` | `onDemand` + +export type ScopedWrite = + | { source: `parts`; type: `put`; row: ScopedPart } + | { source: `refs` | `notes`; type: `put`; row: ScopedRef } + | { source: `parts` | `refs` | `notes`; type: `delete`; id: number } + +export type ScopedScenario = { + shape: ScopedShape + mode: ScopedSourceMode + aliases: ScopedAliases + parts: Array + refs: Array + notes: Array + writes: Array +} + +export function scopedSlots(shape: ScopedShape): Array { + if (shape.topology === `wrapper`) return [`wrapped`, `joinSub`, `joinSource`] + const slots = [`outer`, `sub`, `joined`, `noted`, `include`] + if (shape.topology === `unionBranch`) slots.push(`inactive`) + return [...slots, ...includeBodySlots[shape.include.body]] +} + +const includeBodySlots: Record> = { + plain: [], + joined: [`includeJoin`], + union: [`includeOther`, `includeAnchor`], + nestedFrom: [`includeOuter`], +} + +export function canonicalScopedAliases(shape: ScopedShape): ScopedAliases { + return Object.fromEntries( + scopedSlots(shape).map((slot) => [slot, `canonical_${slot}`]), + ) +} + +/** + * Legality follows the documented lexical rules (ARCHITECTURE.md §Identity), + * not the builder's validator. One scope keeps its names distinct, and no + * scope inside an include can reuse an alias its ancestors can see. Sibling + * scopes are unconstrained. The scopes are: + * + * - subquery: `sub`, plus `joined` and `noted` when those joins exist; + * - outer: `outer`; + * - a union: every alias of every `unionAll()` branch and of the union's own + * joins. The compiler treats them as one namespace, so a union's subquery + * aliases and `inactive` must all differ; + * - include: `include` plus its body's level (`includeJoin`, or + * `includeOther` and `includeAnchor`); a `nestedFrom` body has the + * include level `includeOuter` and the inner body `include`. Every + * include slot is inside the outer row's scope, so none may equal `outer`; + * - wrapper outer: `wrapped` and `joinSub`; wrapped and join bodies: + * `wrapped` and `joinSource`. + * + * A same-scope repeat must be rejected. A shadowing name must be rejected + * with `DuplicateAliasInSubqueryError`. + */ +export type ScopedNaming = `legal` | `sameScope` | `shadowing` + +export function classifyScopedNaming( + shape: ScopedShape, + aliases: ScopedAliases, +): ScopedNaming { + const repeats = (scope: Array) => + new Set(scope).size !== scope.length + if (shape.topology === `wrapper`) { + return aliases.wrapped === aliases.joinSub ? `sameScope` : `legal` + } + const { joins } = shape.subquery + const subScope = [aliases.sub] + if (joins !== `none`) subScope.push(aliases.joined) + if (joins === `refsThenNotes`) subScope.push(aliases.noted) + if (shape.topology === `unionBranch`) subScope.push(aliases.inactive) + const includeScopes: Record< + ScopedInclude[`body`], + Array> + > = { + plain: [[aliases.include]], + joined: [[aliases.include, aliases.includeJoin]], + union: [[aliases.include, aliases.includeOther, aliases.includeAnchor]], + nestedFrom: [[aliases.includeOuter], [aliases.include]], + } + const includeSlots = includeScopes[shape.include.body] + if (repeats(subScope) || includeSlots.some(repeats)) return `sameScope` + return includeSlots.flat().includes(aliases.outer) ? `shadowing` : `legal` +} + +export function isLegalScopedNaming( + shape: ScopedShape, + aliases: ScopedAliases, +): boolean { + return classifyScopedNaming(shape, aliases) === `legal` +} + +const aliasPool = [`a`, `b`, `c`] as const + +const partArbitrary = fc.record({ + id: fc.integer({ min: 1, max: 3 }), + active: fc.boolean(), +}) +const refArbitrary = fc.record({ + id: fc.integer({ min: 1, max: 4 }), + partId: fc.integer({ min: 1, max: 3 }), + clientId: fc.integer({ min: 1, max: 2 }), +}) + +const subqueryArbitrary: fc.Arbitrary = fc + .record({ + joins: fc.constantFrom( + `none` as const, + `refs` as const, + `refsThenNotes` as const, + ), + refsJoin: fc.constantFrom(`left` as const, `inner` as const), + predicates: fc.uniqueArray( + fc.constantFrom(`subActive` as const, `joinedClient` as const), + { maxLength: 2 }, + ), + predicateForm: fc.constantFrom(`separate` as const, `and` as const), + body: fc.constantFrom( + `plain` as const, + `orderedLimit` as const, + `distinct` as const, + ), + subSelect: fc.constantFrom(`row` as const, `fields` as const), + }) + .filter( + (subquery) => + subquery.joins !== `none` || + !subquery.predicates.includes(`joinedClient`), + ) + +const includeArbitrary: fc.Arbitrary = fc.record({ + body: fc.constantFrom( + `plain` as const, + `joined` as const, + `union` as const, + `nestedFrom` as const, + ), + form: fc.constantFrom(`toArray` as const, `materialize` as const), + source: fc.constantFrom(`refs` as const, `notes` as const), + clientFilter: fc.boolean(), + outerSelect: fc.constantFrom(`spread` as const, `fields` as const), +}) + +export const scopedShapeArbitrary: fc.Arbitrary = fc.oneof( + fc.record({ + topology: fc.constantFrom(`fromSubquery` as const, `unionBranch` as const), + subquery: subqueryArbitrary, + include: includeArbitrary, + }), + fc.record({ + topology: fc.constant(`wrapper` as const), + wrapperJoin: fc.constantFrom(`left` as const, `inner` as const), + }), +) + +const writeArbitrary: fc.Arbitrary = fc.oneof( + fc.record({ + source: fc.constant(`parts` as const), + type: fc.constant(`put` as const), + row: partArbitrary, + }), + fc.record({ + source: fc.constantFrom(`refs` as const, `notes` as const), + type: fc.constant(`put` as const), + row: refArbitrary, + }), + fc.record({ + source: fc.constantFrom( + `parts` as const, + `refs` as const, + `notes` as const, + ), + type: fc.constant(`delete` as const), + id: fc.integer({ min: 1, max: 4 }), + }), +) + +const scenarioCandidateArbitrary = scopedShapeArbitrary.chain((shape) => + fc.record({ + shape: fc.constant(shape), + mode: fc.constantFrom(`eager` as const, `onDemand` as const), + aliases: fc.record( + Object.fromEntries( + scopedSlots(shape).map((slot) => [slot, fc.constantFrom(...aliasPool)]), + ) as Record>, + ), + parts: fc.uniqueArray(partArbitrary, { + selector: (row) => row.id, + maxLength: 3, + }), + refs: fc.uniqueArray(refArbitrary, { + selector: (row) => row.id, + maxLength: 4, + }), + notes: fc.uniqueArray(refArbitrary, { + selector: (row) => row.id, + maxLength: 4, + }), + writes: fc.array(writeArbitrary, { maxLength: 4 }), + }), +) + +/** + * Three in four scenarios use a legal naming and check the law. The rest use + * any naming, so illegal ones check that the builder rejects them. + */ +export const scopedScenarioArbitrary: fc.Arbitrary = fc.oneof( + { + arbitrary: scenarioCandidateArbitrary.filter(({ shape, aliases }) => + isLegalScopedNaming(shape, aliases), + ), + weight: 3, + }, + { arbitrary: scenarioCandidateArbitrary, weight: 1 }, +) + +// ## Reference model + +type ModelState = { + parts: Map + refs: Map + notes: Map +} + +export type ScopedResultRow = Record + +/** + * Plain recomputation of the public rows. It reads the current source rows + * and never consults the plan, aliases, or SourceIds, so it judges every + * naming, including the canonical one. + * + * Joins multiply rows: one subquery row exists per surviving join + * combination, and duplicates remain. An absent LEFT match is `undefined`, + * and `eq()` on it is unknown, which a WHERE clause rejects. `orderedLimit` + * keeps the first row by part id; tied rows carry identical part fields. + */ +export function recomputeScopedRows( + shape: ScopedShape, + state: ModelState, +): Array { + const parts = [...state.parts.values()] + const refs = [...state.refs.values()] + const notes = [...state.notes.values()] + + if (shape.topology === `wrapper`) { + const clientOne = refs.filter((ref) => ref.clientId === 1) + return parts.flatMap((part): Array => { + const matches = clientOne.filter((ref) => ref.partId === part.id) + if (matches.length === 0) { + return shape.wrapperJoin === `left` + ? [{ id: part.id, active: part.active, refId: null }] + : [] + } + return matches.map((ref) => ({ + id: part.id, + active: part.active, + refId: ref.id, + })) + }) + } + + const { subquery, include } = shape + type Combination = { part: ScopedPart; joined?: ScopedRef } + const leftMatches = (rows: Array, inner: boolean) => + rows.length > 0 ? rows : inner ? [] : [undefined] + + let combinations: Array = parts.map((part) => ({ part })) + if (subquery.joins !== `none`) { + combinations = combinations.flatMap((combination) => + leftMatches( + refs.filter((ref) => ref.partId === combination.part.id), + subquery.refsJoin === `inner`, + ).map((joined) => ({ ...combination, joined })), + ) + } + if (subquery.joins === `refsThenNotes`) { + combinations = combinations.flatMap((combination) => + leftMatches( + notes.filter((note) => note.partId === combination.part.id), + false, + ).map(() => combination), + ) + } + combinations = combinations.filter((combination) => + subquery.predicates.every((predicate) => + predicate === `subActive` + ? combination.part.active === true + : combination.joined?.clientId === 1, + ), + ) + + let subRows = combinations.map(({ part }) => ({ + id: part.id, + active: part.active, + })) + if (subquery.body === `distinct`) { + subRows = [ + ...new Map(subRows.map((row) => [JSON.stringify(row), row])).values(), + ] + } else if (subquery.body === `orderedLimit`) { + subRows = [...subRows].sort((left, right) => left.id - right.id).slice(0, 1) + } + + const outerRows = + shape.topology === `unionBranch` + ? [ + ...subRows, + ...parts + .filter((part) => part.active !== true) + .map((part) => ({ id: part.id, active: part.active })), + ] + : subRows + + const includeRows = include.source === `refs` ? refs : notes + return outerRows.map((row) => ({ + id: row.id, + active: row.active, + members: includeRows + .filter( + (member) => + member.partId === row.id && + (!include.clientFilter || member.clientId === 1), + ) + .map((member) => ({ id: member.id, clientId: member.clientId })), + })) +} + +// ## Production driver + +/** + * One source fixture per Collection. `eager` installs every row through a + * mock sync. `onDemand` is a finite provider: it installs only rows that + * match a `loadSubset` WHERE clause, records each request, and later + * forwards a write for an installed row or a row that matches a recorded + * request. A request routed to the wrong Collection therefore changes the + * public rows, not only the request log. + */ +export type ScopedSource = { + collection: Collection + requests: Array + put: (row: T) => void + remove: (id: number) => void +} + +let nextScopedSourceId = 0 + +export function createScopedSource( + name: string, + rows: Array, + mode: ScopedSourceMode, +): ScopedSource { + const requests: Array = [] + if (mode === `eager`) { + const options = mockSyncCollectionOptions({ + id: `scoped-${name}-${nextScopedSourceId++}`, + getKey: (row) => row.id, + initialData: rows.map((row) => ({ ...row })), + }) + const collection = createCollection(options) + const send = (type: `insert` | `update` | `delete`, value: T) => { + options.utils.begin() + options.utils.write({ type, value: { ...value } }) + options.utils.commit() + } + return { + collection, + requests, + put: (row) => send(collection.has(row.id) ? `update` : `insert`, row), + remove: (id) => { + const current = collection.get(id) + if (current) send(`delete`, current) + }, + } + } + + const backing = new Map(rows.map((row) => [row.id, { ...row }])) + const installed = new Set() + const predicates: Array<(row: T) => boolean> = [] + let sync: + | { + begin: () => void + write: (change: { + type: `insert` | `update` | `delete` + value: T + }) => void + commit: () => void + } + | undefined + const collection = createCollection({ + id: `scoped-${name}-${nextScopedSourceId++}`, + getKey: (row) => row.id, + syncMode: `on-demand`, + sync: { + sync: ({ begin, write, commit, markReady }) => { + sync = { begin, write, commit } + markReady() + return { + loadSubset: (options: LoadSubsetOptions) => { + requests.push(JSON.stringify(options.where ?? null)) + const matches = options.where + ? createFilterFunctionFromExpression(options.where) + : () => true + predicates.push(matches) + begin() + for (const row of backing.values()) { + if (installed.has(row.id) || !matches(row)) continue + installed.add(row.id) + write({ type: `insert`, value: { ...row } }) + } + commit() + return Promise.resolve() + }, + } + }, + }, + }) + const send = (type: `insert` | `update` | `delete`, value: T) => { + sync!.begin() + sync!.write({ type, value: { ...value } }) + sync!.commit() + } + return { + collection, + requests, + put: (row) => { + backing.set(row.id, { ...row }) + if (installed.has(row.id)) { + send(`update`, row) + } else if (predicates.some((matches) => matches(row))) { + installed.add(row.id) + send(`insert`, row) + } + }, + remove: (id) => { + const current = backing.get(id) + backing.delete(id) + if (current && installed.delete(id)) send(`delete`, current) + }, + } +} + +export type ScopedSources = { + parts: ScopedSource + refs: ScopedSource + notes: ScopedSource +} + +type Context = Record + +/** + * Builds the live query for one shape and one naming. Alias names are data, + * so callbacks read the namespaced context by key. Every naming of one shape + * builds the same plan up to alias text. + */ +export function createScopedQuery( + shape: ScopedShape, + aliases: ScopedAliases, + sources: ScopedSources, +) { + const n = aliases + return createLiveQueryCollection((q) => { + if (shape.topology === `wrapper`) { + const clientOne = q + .from({ [n.joinSource!]: sources.refs.collection }) + .where((c: Context) => eq(c[n.joinSource!].clientId, 1)) + .select((c: Context) => ({ + partId: c[n.joinSource!].partId, + refId: c[n.joinSource!].id, + })) + return ( + q.from({ + [n.wrapped!]: q.from({ [n.wrapped!]: sources.parts.collection }), + } as any) as any + ) + .join( + { [n.joinSub!]: clientOne }, + (c: Context) => eq(c[n.joinSub!].partId, c[n.wrapped!].id), + shape.wrapperJoin, + ) + .select((c: Context) => ({ + id: c[n.wrapped!].id, + active: c[n.wrapped!].active, + refId: c[n.joinSub!].refId, + })) + } + + const { subquery, include } = shape + let sub: any = q.from({ [n.sub!]: sources.parts.collection }) + if (subquery.joins !== `none`) { + sub = sub.join( + { [n.joined!]: sources.refs.collection }, + (c: Context) => eq(c[n.joined!].partId, c[n.sub!].id), + subquery.refsJoin, + ) + } + if (subquery.joins === `refsThenNotes`) { + sub = sub.join( + { [n.noted!]: sources.notes.collection }, + (c: Context) => eq(c[n.noted!].partId, c[n.sub!].id), + `left`, + ) + } + const predicates = subquery.predicates.map((predicate) => + predicate === `subActive` + ? (c: Context) => eq(c[n.sub!].active, true) + : (c: Context) => eq(c[n.joined!].clientId, 1), + ) + if (subquery.predicateForm === `and` && predicates.length > 1) { + sub = sub.where((c: Context) => and(predicates[0]!(c), predicates[1]!(c))) + } else { + for (const predicate of predicates) sub = sub.where(predicate) + } + if (subquery.body === `orderedLimit`) { + sub = sub.orderBy((c: Context) => c[n.sub!].id).limit(1) + } + sub = sub.select((c: Context) => + subquery.subSelect === `row` + ? c[n.sub!] + : { id: c[n.sub!].id, active: c[n.sub!].active }, + ) + if (subquery.body === `distinct`) sub = sub.distinct() + + const outerSource = + shape.topology === `unionBranch` + ? q.unionAll( + sub, + q + .from({ [n.inactive!]: sources.parts.collection }) + .where((c: Context) => not(eq(c[n.inactive!].active, true))) + .select((c: Context) => ({ + id: c[n.inactive!].id, + active: c[n.inactive!].active, + })), + ) + : sub + const includeCollection = + include.source === `refs` + ? sources.refs.collection + : sources.notes.collection + + return (q.from({ [n.outer!]: outerSource } as any) as any).select( + (c: Context) => { + const outer = c[n.outer!] + const clientOne = (member: any) => eq(member.clientId, 1) + let child: any + if (include.body === `union`) { + const branch = (alias: string, keep: (member: any) => any) => + q + .from({ [alias]: includeCollection }) + .where((cc: Context) => keep(cc[alias])) + .select((cc: Context) => ({ memberId: cc[alias].id })) + child = q + .unionAll( + branch(n.include!, clientOne), + branch(n.includeOther!, (member) => not(clientOne(member))), + ) + .innerJoin( + { [n.includeAnchor!]: includeCollection }, + (cc: Context) => eq(cc.memberId, cc[n.includeAnchor!].id), + ) + .where((cc: Context) => eq(cc[n.includeAnchor!].partId, outer.id)) + if (include.clientFilter) { + child = child.where((cc: Context) => + clientOne(cc[n.includeAnchor!]), + ) + } + child = child.select((cc: Context) => ({ + id: cc[n.includeAnchor!].id, + clientId: cc[n.includeAnchor!].clientId, + })) + } else { + // `member` is the alias whose row the include selects. + const member = + include.body === `nestedFrom` ? n.includeOuter! : n.include! + child = + include.body === `nestedFrom` + ? q.from({ + [member]: q + .from({ [n.include!]: includeCollection }) + .select((cc: Context) => ({ + id: cc[n.include!].id, + partId: cc[n.include!].partId, + clientId: cc[n.include!].clientId, + parentId: outer.id, + })), + }) + : q.from({ [member]: includeCollection }) + if (include.body === `joined`) { + child = child.join( + { [n.includeJoin!]: sources.parts.collection }, + (cc: Context) => eq(cc[n.includeJoin!].id, cc[member].partId), + `inner`, + ) + } + child = child.where((cc: Context) => eq(cc[member].partId, outer.id)) + if (include.clientFilter) { + child = child.where((cc: Context) => clientOne(cc[member])) + } + child = child.select((cc: Context) => ({ + id: cc[member].id, + clientId: cc[member].clientId, + })) + } + const members = + include.form === `toArray` ? toArray(child) : materialize(child) + return include.outerSelect === `spread` + ? { ...outer, members } + : { id: outer.id, active: outer.active, members } + }, + ) + }) +} + +// ## Observation + +/** + * Neither query promises an order, so rows and include members are compared + * as bags of complete values. Virtual `$` fields are delivery metadata and + * are removed; an absent LEFT-joined field is compared as `null`. + */ +export function normalizeScopedRows(rows: Array): Array { + const normalize = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map((entry) => JSON.stringify(normalize(entry))).sort() + } + if (!value || typeof value !== `object`) return value ?? null + return Object.fromEntries( + Object.entries(value) + .filter(([key]) => !key.startsWith(`$`)) + .map(([key, entry]) => [key, normalize(entry)]), + ) + } + return rows.map((row) => JSON.stringify(normalize(row))).sort() +} + +export function createScopedSources(scenario: ScopedScenario): ScopedSources { + return { + parts: createScopedSource(`parts`, scenario.parts, scenario.mode), + refs: createScopedSource(`refs`, scenario.refs, scenario.mode), + notes: createScopedSource(`notes`, scenario.notes, scenario.mode), + } +} + +export function createModelState(scenario: ScopedScenario): ModelState { + return { + parts: new Map(scenario.parts.map((row) => [row.id, { ...row }])), + refs: new Map(scenario.refs.map((row) => [row.id, { ...row }])), + notes: new Map(scenario.notes.map((row) => [row.id, { ...row }])), + } +} + +export function applyScopedWrite( + sourceSets: Array, + state: ModelState, + write: ScopedWrite, +): void { + if (write.type === `delete`) { + state[write.source].delete(write.id) + for (const sources of sourceSets) sources[write.source].remove(write.id) + return + } + if (write.source === `parts`) { + state.parts.set(write.row.id, { ...write.row }) + for (const sources of sourceSets) sources.parts.put(write.row) + } else { + state[write.source].set(write.row.id, { ...write.row }) + for (const sources of sourceSets) sources[write.source].put(write.row) + } +} + +export function sortedRequests(sources: ScopedSources) { + return { + parts: [...sources.parts.requests].sort(), + refs: [...sources.refs.requests].sort(), + notes: [...sources.notes.requests].sort(), + } +} diff --git a/packages/db/tests/query/validate-aliases.test.ts b/packages/db/tests/query/validate-aliases.test.ts index bdfa2955fd..9c1169bad3 100644 --- a/packages/db/tests/query/validate-aliases.test.ts +++ b/packages/db/tests/query/validate-aliases.test.ts @@ -1,5 +1,9 @@ import { beforeEach, describe, expect, test } from 'vitest' -import { createLiveQueryCollection, eq } from '../../src/query/index.js' +import { + createLiveQueryCollection, + eq, + toArray, +} from '../../src/query/index.js' import { createCollection } from '../../src/collection/index.js' import { mockSyncCollectionOptions } from '../utils.js' @@ -98,6 +102,227 @@ describe(`Alias validation in subqueries`, () => { }).toThrow(/Subquery uses alias "lock"/) }) + // The include sees the outer subquery alias, so reusing it would shadow the + // parent row. Before this check the correlation silently read the include's + // own source and returned empty children. + test(`should throw DuplicateAliasInSubqueryError when an include reuses a parent subquery alias`, () => { + expect(() => { + createLiveQueryCollection({ + startSync: true, + query: (q) => { + const namedLocks = q + .from({ source: locksCollection }) + .select(({ source }) => ({ _id: source._id, name: source.name })) + return q + .from({ lock: namedLocks }) + .select(({ lock: parentLock }) => ({ + _id: parentLock._id, + votes: q + .from({ lock: votesCollection }) + .where(({ lock: childLock }) => + eq(childLock.lockId, parentLock._id), + ), + })) + }, + }) + }).toThrow(/Subquery uses alias "lock"/) + }) + + test(`should throw DuplicateAliasInSubqueryError when a nested include reuses a grandparent subquery alias`, () => { + expect(() => { + createLiveQueryCollection({ + startSync: true, + query: (q) => { + const namedLocks = q + .from({ source: locksCollection }) + .select(({ source }) => ({ _id: source._id, name: source.name })) + return q.from({ lock: namedLocks }).select(({ lock }) => ({ + _id: lock._id, + votes: toArray( + q + .from({ vote: votesCollection }) + .where(({ vote }) => eq(vote.lockId, lock._id)) + .select(({ vote }) => ({ + _id: vote._id, + siblings: toArray( + q + .from({ lock: votesCollection }) + .where(({ lock: sibling }) => + eq(sibling.lockId, vote.lockId), + ) + .select(({ lock: sibling }) => ({ _id: sibling._id })), + ), + })), + ), + })) + }, + }) + }).toThrow(/Subquery uses alias "lock"/) + }) + + // A unionAll() branch inside an include is part of the include's scope, so + // its aliases cannot shadow the parent row either. + test(`should throw DuplicateAliasInSubqueryError when an include's unionAll branch reuses a parent subquery alias`, () => { + expect(() => { + createLiveQueryCollection({ + startSync: true, + query: (q) => { + const namedLocks = q + .from({ source: locksCollection }) + .select(({ source }) => ({ _id: source._id, name: source.name })) + return q + .from({ lock: namedLocks }) + .select(({ lock: parentLock }) => ({ + _id: parentLock._id, + votes: toArray( + q + .unionAll( + q + .from({ lock: votesCollection }) + .select(({ lock: vote }) => ({ voteId: vote._id })), + q + .from({ other: votesCollection }) + .select(({ other }) => ({ voteId: other._id })), + ) + .innerJoin( + { anchor: votesCollection }, + ({ voteId, anchor }) => eq(voteId, anchor._id), + ) + .where(({ anchor }) => eq(anchor.lockId, parentLock._id)) + .select(({ voteId }) => ({ voteId })), + ), + })) + }, + }) + }).toThrow(/Subquery uses alias "lock"/) + }) + + // A from() subquery inside an include can read the parent row through its + // callbacks, so it cannot shadow the parent alias either. + test(`should throw DuplicateAliasInSubqueryError when an include's from() subquery reuses a parent subquery alias`, () => { + expect(() => { + createLiveQueryCollection({ + startSync: true, + query: (q) => { + const namedLocks = q + .from({ source: locksCollection }) + .select(({ source }) => ({ _id: source._id, name: source.name })) + return q + .from({ lock: namedLocks }) + .select(({ lock: parentLock }) => ({ + _id: parentLock._id, + votes: toArray( + q + .from({ + vote: q + .from({ lock: votesCollection }) + .select(({ lock }) => ({ + voteId: lock._id, + lockId: lock.lockId, + lockName: parentLock.name, + })), + }) + .where(({ vote }) => eq(vote.lockId, parentLock._id)) + .select(({ vote }) => ({ voteId: vote.voteId })), + ), + })) + }, + }) + }).toThrow(/Subquery uses alias "lock"/) + }) + + test(`should throw DuplicateAliasInSubqueryError when a from() subquery inside an include's unionAll branch reuses a parent subquery alias`, () => { + expect(() => { + createLiveQueryCollection({ + startSync: true, + query: (q) => { + const namedLocks = q + .from({ source: locksCollection }) + .select(({ source }) => ({ _id: source._id, name: source.name })) + return q + .from({ lock: namedLocks }) + .select(({ lock: parentLock }) => ({ + _id: parentLock._id, + votes: toArray( + q + .unionAll( + q + .from({ + mine: q + .from({ lock: votesCollection }) + .select(({ lock }) => ({ + voteId: lock._id, + lockName: parentLock.name, + })), + }) + .select(({ mine }) => ({ voteId: mine.voteId })), + q + .from({ other: votesCollection }) + .select(({ other }) => ({ voteId: other._id })), + ) + .innerJoin( + { anchor: votesCollection }, + ({ voteId, anchor }) => eq(voteId, anchor._id), + ) + .where(({ anchor }) => eq(anchor.lockId, parentLock._id)) + .select(({ voteId }) => ({ voteId })), + ), + })) + }, + }) + }).toThrow(/Subquery uses alias "lock"/) + }) + + test(`should reject two joins that use the same alias in one query`, () => { + expect(() => { + createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ lock: locksCollection }) + .join({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id), + ) + .join({ vote: votesCollection }, ({ lock, vote }) => + eq(vote.lockId, lock._id), + ) + .select(({ lock }) => ({ _id: lock._id })), + }) + }).toThrow(/alias "vote" more than once/) + }) + + test(`should allow an include to reuse an alias from a sibling from() subquery`, async () => { + const live = createLiveQueryCollection({ + startSync: true, + query: (q) => { + const namedLocks = q + .from({ vote: locksCollection }) + .select(({ vote }) => ({ _id: vote._id, name: vote.name })) + return q.from({ lock: namedLocks }).select(({ lock }) => ({ + _id: lock._id, + votes: toArray( + q + .from({ vote: votesCollection }) + .where(({ vote }) => eq(vote.lockId, lock._id)) + .select(({ vote }) => ({ _id: vote._id })), + ), + })) + }, + }) + await live.preload() + expect( + live.toArray + .map((row) => ({ + _id: row._id, + votes: [...row.votes].map((vote) => vote._id).sort(), + })) + .sort((left, right) => left._id - right._id), + ).toEqual([ + { _id: 1, votes: [1, 2] }, + { _id: 2, votes: [3] }, + ]) + }) + test(`should allow subqueries when all collection aliases are unique`, () => { const query = createLiveQueryCollection({ startSync: true, From 62d7a8fee495d7fd1e4f4163b0d86b9a14c9fba5 Mon Sep 17 00:00:00 2001 From: Isaac Date: Thu, 1 Oct 2026 10:41:02 -0600 Subject: [PATCH 2/2] fix(db): let includes reuse unionAll branch aliases A union row holds the branches' projected fields, not their aliases, so an include or join on a unionAll() query cannot see branch aliases. The include-shadowing check exposed them anyway and rejected a query that main accepts with correct rows. Includes now see only the parent's from and join aliases. Branch aliases inside an include are still checked against its ancestors. Adds a unionParent oracle topology and pinned witnesses for both legal union namings, and corrects the union scope rule in ARCHITECTURE.md. Co-authored-by: Isaac --- docs/contributing/oracle-coverage.md | 2 +- .../issue-1975-scope-identity.md | 7 ++ packages/db/src/query/compiler/index.ts | 7 +- packages/db/src/query/live/ARCHITECTURE.md | 8 +- .../query/includes-oracle.property.test.ts | 12 ++- .../query/includes-scope-identity-oracle.ts | 91 +++++++++++++++++-- .../db/tests/query/validate-aliases.test.ts | 74 +++++++++++++++ 7 files changed, 184 insertions(+), 17 deletions(-) diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index b3055f8fd3..8fa79fe73e 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -249,7 +249,7 @@ comment and the current API/architecture contract before extending its model. | Indexed predicate filtering | `packages/db/tests/query/index-path-collision-oracle.test.ts` | A bounded cross product of numeric values checks exact public keys before and after adding a nested-field BTree index. Direct predicates vary argument order, bound inclusivity, reversed operands, and bound field. Public Collection subscription and live-query callbacks run with and without the index. Selected-field ordering compares a direct JavaScript sort with a public `$selected` callback; restoring the dotted-key proxy cache made that comparison fail. The original compound grouping failed four indexed cases while scan controls passed. The original callback proxy caches also failed both unindexed public routes. A hostile cleanup control preserves the primary mismatch and secondary release error. Arbitrary path segments, nullish values, custom collation, and incremental publication need separate witnesses if claimed. | | Lazy target path identity | `packages/db/tests/query/compiler/lazy-targets.test.ts` | A focused same-source `UnionFrom`/`coalesce` witness requires both [`a.b`] and [`a`, `b`] demand targets. Restoring dotted-string deduplication drops the second target at the compiler boundary. A public on-demand adapter and row-publication history still need a separate witness. | | Correlated include path identity | `packages/db/tests/query/includes-context-transport-oracle.test.ts` | A flat parent field and nested parent field reach one-level and nested `toArray` results independently; dotted ancestor aliases remain distinct through a grandchild route. Initial results and parent updates have exact public-value checks. A conditional projection gives [`a.b`] and [`a`, `b`] different include results and checks parent-key changes plus later child inserts. Removing the compiler's unique route-key allocator loses a public child result; restoring dotted-string deduplication at either builder site loses a distinct parent value. These fixed witnesses do not establish arbitrary path segments, every recursive source form, or every materialization form. | -| Alias scope identity | `packages/db/tests/query/includes-oracle.property.test.ts` (`includes alpha-renaming across sibling scopes`), `packages/db/tests/query/includes-scope-identity-oracle.ts`, `packages/db/tests/query/validate-aliases.test.ts`, [issue #1975 review](oracle-reviews/issue-1975-scope-identity.md) | Three topologies place a rewritable source beside a same-named sibling source: a `from()` subquery with an include, a joined `unionAll()` branch with an include, and a collapsed top-level pure wrapper with a join subquery. The grammar crosses zero to two joins, LEFT or INNER, zero to two predicates, plain, ordered-and-limited, or DISTINCT bodies, include forms, and eager or on-demand finite providers, after preload and after source writes. Includes read their source directly, through a join, through a `unionAll()` with an anchor join, or through a `from()` subquery that reads the parent row. One in four scenarios draws any naming and requires illegal ones to be rejected. Each checkpoint compares the canonical naming with plain recomputation, the generated naming with the canonical naming, and per-Collection `loadSubset` WHERE clauses. Every pinned witness and both campaigns fail on `18abceee4`; each of the four optimizer re-mint sites fails a witness when reverted. Nested includes, include-inside-subquery forms, join subqueries outside the wrapper topology, `groupBy`/`having`, outer RIGHT/FULL joins, publication events, unload, ordered windows and lazy join loading through the alias-merged `aliasRemapping`, and temporal demand remain outside this owner; they need witnesses before an alias-scope closure claim covers them. | +| Alias scope identity | `packages/db/tests/query/includes-oracle.property.test.ts` (`includes alpha-renaming across sibling scopes`), `packages/db/tests/query/includes-scope-identity-oracle.ts`, `packages/db/tests/query/validate-aliases.test.ts`, [issue #1975 review](oracle-reviews/issue-1975-scope-identity.md) | Four topologies place a source beside a same-named sibling source: a `from()` subquery with an include, a joined `unionAll()` branch with an include, a collapsed top-level pure wrapper with a join subquery, and an include directly on a joined `unionAll()`, which may reuse a branch alias. The grammar crosses zero to two joins, LEFT or INNER, zero to two predicates, plain, ordered-and-limited, or DISTINCT bodies, include forms, and eager or on-demand finite providers, after preload and after source writes. Includes read their source directly, through a join, through a `unionAll()` with an anchor join, or through a `from()` subquery that reads the parent row. One in four scenarios draws any naming and requires illegal ones to be rejected. Each checkpoint compares the canonical naming with plain recomputation, the generated naming with the canonical naming, and per-Collection `loadSubset` WHERE clauses. Every pinned witness and both campaigns fail on `18abceee4`; each of the four optimizer re-mint sites fails a witness when reverted. Nested includes, include-inside-subquery forms, join subqueries outside the wrapper topology, `groupBy`/`having`, outer RIGHT/FULL joins, publication events, unload, ordered windows and lazy join loading through the alias-merged `aliasRemapping`, and temporal demand remain outside this owner; they need witnesses before an alias-scope closure claim covers them. | | Join equality and cold acquisition | `packages/db/tests/query/cold-join-reconciliation-oracle.test.ts` | Independent recomputation for cold acquisition plus direct join/predicate equivalence across established equality domains. Binary/string and nullish classes, replacement histories, raw on-demand values, and both scan/auto-index paths are explicit; compound join syntax is not claimed. | | Optimizer aggregate pushdown | `packages/db/tests/query/optimizer-semantics-oracle.test.ts`, [review record](oracle-reviews/2026-09-28-optimizer-aggregate-pushdown.md), [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | Independent sum recomputation and a materialized-Collection formulation check the first public snapshot of a nested aggregate under a left join. Direct, arithmetic-wrapped, and conditional aggregates distinguish safe from unsafe pushdown; a grouped-key control covers the grouped boundary. Matching and nonmatching global aggregates also cross a nested QueryRef inner join with absent, accepting, and rejecting outer predicates. The matching initial cases fail on the pre-repair revision and pass after the computed-projection lookup repair. Other predicates, wrappers, join forms, and incremental optimizer histories remain outside this owner. | | QueryRef operators and user-value boundaries | `packages/db/tests/query/subquery-user-value-oracle.test.ts`, `packages/db/tests/transactions.test.ts`, [repair record](oracle-reviews/2026-09-30-queryref-publication-repair.md) | A finite array/Set model checks the compiled output bag for a joined DISTINCT subquery with an outer WHERE. Live-query drivers check selected containers, no-select, functional, and predicate-literal user objects with IR-like fields, top-level and aggregate-subquery proxy-shaped fields, arrays of selected references, outer virtual-field filters on joined DISTINCT and nested aggregate QueryRefs, and a renamed no-select source. The transaction suite documents the existing development-browser duplicate-load guard. The bounded cases run in `test:oracles` or the full DB suite. Joined DISTINCT, nested and flat aggregate source-update histories pass. Ordered joined `findOne()` checks initial, singleton, empty, restored, and changed-join-key cuts; an unordered `findOne()` QueryRef on either side of a join checks default-key candidate deletion and reinsertion. A materialized joined `findOne()` supplies a separate receiving formulation. Other `singleResult` forms, join forms, schedules, and cross-copy value handling outside the duplicate-load guard remain unproved. | diff --git a/docs/contributing/oracle-reviews/issue-1975-scope-identity.md b/docs/contributing/oracle-reviews/issue-1975-scope-identity.md index 5818ba196c..f14e103432 100644 --- a/docs/contributing/oracle-reviews/issue-1975-scope-identity.md +++ b/docs/contributing/oracle-reviews/issue-1975-scope-identity.md @@ -36,6 +36,12 @@ lookup returned a sibling scope's source with the same name. Top-level `from()` subqueries see no ancestor, so they may still reuse names. - `compiler/index.ts` `validateQueryStructure`: one query cannot give two of its sources the same alias. Two joins with one alias were accepted before. +- An include sees its ancestors' from and join aliases, but not the aliases + inside a parent's `unionAll()` branches. A union row holds the branches' + projected fields, so those names belong to sibling scopes. An earlier + revision of this change rejected an include that reused a branch alias, which + base `18abceee4` accepted with correct rows. The `unionParent` topology now + guards that legal naming. ## Bug-class boundary @@ -64,6 +70,7 @@ lookup returned a sibling scope's source with the same name. | Identity-only binding | With it, each optimizer site mutant also fails the existing suite with `CollectionInputNotFoundError` (3, 53, 146, and 59 tests) instead of returning wrong rows. With the optimizer repair in place it is behavior-equivalent; its value is converting a future identity loss into an error. | | Include shadowing | Four rejection witnesses (include, nested include, include `unionAll()` branch, include `from()` subquery) fail on `18abceee4` and pass with the repair; a sibling-reuse control passes on both. | | Generated rejection | One in four scenarios draws any naming; illegal ones must be rejected, and shadowing ones with `DuplicateAliasInSubqueryError`. At a 10x budget this check found the duplicate-join acceptance, and it fails when include `from()` visibility or the same-scope check is reverted. Reverting union-branch visibility is caught only by its pinned `validate-aliases.test.ts` witness, because the include level already checks branch aliases. | +| Union scopes | Reintroducing branch aliases into an include's visible set fails both `unionParent` witnesses and both campaigns at the default budget. A derived branch alias and a same-named union join return the same rows as a renamed join; `validate-aliases.test.ts` pins both legal namings. | | Model calibration | Planted model faults (ignore child filter, LEFT as INNER, drop join duplicates) fail only at "canonical naming against recomputation". | | Request calibration | An alias-dependent WHERE routing mutant fails only at "loadSubset requests per Collection". | | Ablation | Restricting the earlier grammar to all-distinct names made both campaigns pass on the unrepaired code; the failure needs cross-scope reuse. | diff --git a/packages/db/src/query/compiler/index.ts b/packages/db/src/query/compiler/index.ts index 8a485c000a..2380ac8715 100644 --- a/packages/db/src/query/compiler/index.ts +++ b/packages/db/src/query/compiler/index.ts @@ -1408,10 +1408,9 @@ function validateQueryStructure( // An include sees every alias of its ancestors, including subquery // aliases, so it cannot shadow any of them. if (query.select) { - const scopeAliases = new Set([ - ...visibleAliases, - ...collectScopeAliases(query), - ]) + // A parent row exposes its from and join aliases, not the aliases inside + // its unionAll() branches. + const scopeAliases = new Set([...visibleAliases, ...levelAliases]) for (const { subquery } of extractIncludesFromSelect(query.select)) { validateQueryStructure(subquery.query, combinedAliases, scopeAliases) } diff --git a/packages/db/src/query/live/ARCHITECTURE.md b/packages/db/src/query/live/ARCHITECTURE.md index 84076f0fa0..e91d6202ce 100644 --- a/packages/db/src/query/live/ARCHITECTURE.md +++ b/packages/db/src/query/live/ARCHITECTURE.md @@ -159,8 +159,10 @@ A plan rewrite must preserve each Collection reference's `SourceId`. The optimizer reuses the reference when it copies, wraps, or collapses a source, and compilation reads source inputs by `SourceId` only. A source whose identity was lost raises `CollectionInputNotFoundError`; it never reads a same-named -source from another scope. Every alias in every `unionAll()` branch belongs to -one namespace, and the compiler rejects a repeated name. +source from another scope. The branches of one `unionAll()` share an alias +namespace, and the compiler rejects a name that two branches repeat. A union row +holds the branches' projected fields, so a branch alias is not visible to the +union's own joins or includes. A `CanonicalCorrelationKey` is the canonical tuple of every evaluated parent-dependent value that can affect the child plan. This includes values @@ -1258,7 +1260,7 @@ create recursive Collection machinery. change an explicitly projected result. An implicit namespaced result keeps its aliases as public field names. Aliases must be unique within one lexical scope and cannot shadow an ancestor alias. Sibling scopes may reuse aliases, - except that all `unionAll()` branches share one alias namespace. + except that the branches of one `unionAll()` share one alias namespace. 2. **Contribution conservation:** a public row exists exactly when its reduced supporting weight and collision policy produce one. 3. **Batch partition:** equivalent valid split and atomic deliveries converge. diff --git a/packages/db/tests/query/includes-oracle.property.test.ts b/packages/db/tests/query/includes-oracle.property.test.ts index 94fcfa30d7..ed8ed1c3ea 100644 --- a/packages/db/tests/query/includes-oracle.property.test.ts +++ b/packages/db/tests/query/includes-oracle.property.test.ts @@ -5795,8 +5795,9 @@ function pinnedScenario( } /** - * Each pinned witness failed on the unrepaired optimizer and passes with the - * repair. Together they reach every optimizer site that once re-minted a + * Each optimizer witness failed on the unrepaired optimizer and passes with + * the repair. The joined-union witness guards a legal naming that an + * overbroad shadowing check once rejected. Together they reach every optimizer site that once re-minted a * SourceId; the review record maps each site to its killing witness. */ const pinnedScopeWitnesses: Array<[string, ScopedScenario]> = [ @@ -5865,6 +5866,13 @@ const pinnedScopeWitnesses: Array<[string, ScopedScenario]> = [ }, ), ], + [ + `an include on a joined union reuses a branch alias`, + pinnedScenario( + { topology: `unionParent`, includeSource: `refs` }, + { activeBranch: `x`, inactiveBranch: `y`, anchor: `a`, include: `x` }, + ), + ], [ `a join subquery reuses the alias of a collapsed pure wrapper`, pinnedScenario( diff --git a/packages/db/tests/query/includes-scope-identity-oracle.ts b/packages/db/tests/query/includes-scope-identity-oracle.ts index 588892f8d8..6329ae8788 100644 --- a/packages/db/tests/query/includes-scope-identity-oracle.ts +++ b/packages/db/tests/query/includes-scope-identity-oracle.ts @@ -33,9 +33,9 @@ export type ScopedRef = { id: number; partId: number; clientId: number } // ## Grammar /** - * Three topologies place a rewritable source and a same-named source in - * sibling scopes. Each one reached a lost-SourceId failure on the - * unrepaired optimizer: + * Four topologies place a source beside a same-named source in a sibling + * scope. The first three reached a lost-SourceId failure on the unrepaired + * optimizer: * * - `fromSubquery`: an outer `from()` subquery and a correlated include. * - `unionBranch`: the same subquery as one `unionAll()` branch, beside an @@ -44,8 +44,15 @@ export type ScopedRef = { id: number; partId: number; clientId: number } * beside a join subquery. Predicate pushdown would restructure the wrapper * first, so this topology has no WHERE clause; it reaches the redundant * subquery collapse. + * - `unionParent`: an include placed directly on a joined `unionAll()`. A + * union row holds projected fields, so the include may reuse a branch + * alias but not the anchor join alias. */ -export type ScopedTopology = `fromSubquery` | `unionBranch` | `wrapper` +export type ScopedTopology = + | `fromSubquery` + | `unionBranch` + | `wrapper` + | `unionParent` export type ScopedSubquery = { /** Joins of the subquery source: none, refs, or refs then notes. */ @@ -80,6 +87,7 @@ export type ScopedShape = include: ScopedInclude } | { topology: `wrapper`; wrapperJoin: `left` | `inner` } + | { topology: `unionParent`; includeSource: `refs` | `notes` } /** * Alias slots. `fromSubquery` and `unionBranch` use `outer`, `sub`, @@ -109,6 +117,9 @@ export type ScopedScenario = { export function scopedSlots(shape: ScopedShape): Array { if (shape.topology === `wrapper`) return [`wrapped`, `joinSub`, `joinSource`] + if (shape.topology === `unionParent`) { + return [`activeBranch`, `inactiveBranch`, `anchor`, `include`] + } const slots = [`outer`, `sub`, `joined`, `noted`, `include`] if (shape.topology === `unionBranch`) slots.push(`inactive`) return [...slots, ...includeBodySlots[shape.include.body]] @@ -135,9 +146,10 @@ export function canonicalScopedAliases(shape: ScopedShape): ScopedAliases { * * - subquery: `sub`, plus `joined` and `noted` when those joins exist; * - outer: `outer`; - * - a union: every alias of every `unionAll()` branch and of the union's own - * joins. The compiler treats them as one namespace, so a union's subquery - * aliases and `inactive` must all differ; + * - a union: every alias of every `unionAll()` branch. The compiler rejects a + * name that two branches repeat, so a union's subquery aliases and + * `inactive` must all differ. A union row holds projected fields, so the + * union's own joins and includes do not see branch aliases; * - include: `include` plus its body's level (`includeJoin`, or * `includeOther` and `includeAnchor`); a `nestedFrom` body has the * include level `includeOuter` and the inner body `include`. Every @@ -159,6 +171,13 @@ export function classifyScopedNaming( if (shape.topology === `wrapper`) { return aliases.wrapped === aliases.joinSub ? `sameScope` : `legal` } + if (shape.topology === `unionParent`) { + // Branches share one namespace. The builder's existing rule rejects a + // branch that reuses the anchor, a parent Collection alias. + const { activeBranch, inactiveBranch, anchor, include } = aliases + if (repeats([activeBranch, inactiveBranch, anchor])) return `sameScope` + return include === anchor ? `shadowing` : `legal` + } const { joins } = shape.subquery const subScope = [aliases.sub] if (joins !== `none`) subScope.push(aliases.joined) @@ -170,6 +189,8 @@ export function classifyScopedNaming( > = { plain: [[aliases.include]], joined: [[aliases.include, aliases.includeJoin]], + // The anchor join is a sibling of the branches, but the builder's existing + // rule rejects a nested query that reuses a parent Collection alias. union: [[aliases.include, aliases.includeOther, aliases.includeAnchor]], nestedFrom: [[aliases.includeOuter], [aliases.include]], } @@ -246,6 +267,10 @@ export const scopedShapeArbitrary: fc.Arbitrary = fc.oneof( topology: fc.constant(`wrapper` as const), wrapperJoin: fc.constantFrom(`left` as const, `inner` as const), }), + fc.record({ + topology: fc.constant(`unionParent` as const), + includeSource: fc.constantFrom(`refs` as const, `notes` as const), + }), ) const writeArbitrary: fc.Arbitrary = fc.oneof( @@ -337,6 +362,18 @@ export function recomputeScopedRows( const refs = [...state.refs.values()] const notes = [...state.notes.values()] + if (shape.topology === `unionParent`) { + // The branches partition parts by `active`; the anchor matches each once. + const members = shape.includeSource === `refs` ? refs : notes + return parts.map((part) => ({ + id: part.id, + active: part.active, + members: members + .filter((member) => member.partId === part.id) + .map((member) => ({ id: member.id, clientId: member.clientId })), + })) + } + if (shape.topology === `wrapper`) { const clientOne = refs.filter((ref) => ref.clientId === 1) return parts.flatMap((part): Array => { @@ -554,6 +591,46 @@ export function createScopedQuery( ) { const n = aliases return createLiveQueryCollection((q) => { + if (shape.topology === `unionParent`) { + const branch = (alias: string, active: boolean) => + q + .from({ [alias]: sources.parts.collection }) + .where((c: Context) => + active ? eq(c[alias].active, true) : not(eq(c[alias].active, true)), + ) + .select((c: Context) => ({ + id: c[alias].id, + active: c[alias].active, + })) + const members = + shape.includeSource === `refs` + ? sources.refs.collection + : sources.notes.collection + return q + .unionAll( + branch(n.activeBranch!, true), + branch(n.inactiveBranch!, false), + ) + .innerJoin({ [n.anchor!]: sources.parts.collection }, (c: Context) => + eq(c.id, c[n.anchor!].id), + ) + .select((c: Context) => ({ + id: c.id, + active: c.active, + members: toArray( + q + .from({ [n.include!]: members }) + .where((cc: Context) => + eq(cc[n.include!].partId, c[n.anchor!].id), + ) + .select((cc: Context) => ({ + id: cc[n.include!].id, + clientId: cc[n.include!].clientId, + })), + ), + })) + } + if (shape.topology === `wrapper`) { const clientOne = q .from({ [n.joinSource!]: sources.refs.collection }) diff --git a/packages/db/tests/query/validate-aliases.test.ts b/packages/db/tests/query/validate-aliases.test.ts index 9c1169bad3..0a20a53937 100644 --- a/packages/db/tests/query/validate-aliases.test.ts +++ b/packages/db/tests/query/validate-aliases.test.ts @@ -291,6 +291,80 @@ describe(`Alias validation in subqueries`, () => { }).toThrow(/alias "vote" more than once/) }) + // A unionAll() parent row holds the branches' projected fields, not their + // aliases, so an include may reuse a branch alias. + test(`should allow an include under a unionAll() parent to reuse a branch alias`, async () => { + const live = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .unionAll( + q + .from({ item: locksCollection }) + .select(({ item }) => ({ rid: item._id })), + q + .from({ tool: votesCollection }) + .select(({ tool }) => ({ rid: tool._id })), + ) + .innerJoin({ anchor: locksCollection }, ({ rid, anchor }) => + eq(rid, anchor._id), + ) + .select(({ rid, anchor }) => ({ + rid, + votes: toArray( + q + .from({ item: votesCollection }) + .where(({ item }) => eq(item.lockId, anchor._id)) + .select(({ item }) => ({ _id: item._id })), + ), + })), + }) + await live.preload() + expect( + live.toArray + .map((row) => ({ + rid: row.rid, + votes: [...row.votes].map((vote) => vote._id).sort(), + })) + .sort((left, right) => left.rid - right.rid), + ).toEqual([ + { rid: 1, votes: [1, 2] }, + { rid: 1, votes: [1, 2] }, + { rid: 2, votes: [3] }, + { rid: 2, votes: [3] }, + ]) + }) + + // The same holds for the union's own joins: a branch alias is not visible + // to them. + test(`should allow a unionAll() join to reuse an alias of a derived branch source`, async () => { + const live = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .unionAll( + q + .from({ + vote: q + .from({ inner: votesCollection }) + .select(({ inner }) => ({ id: inner._id })), + }) + .select(({ vote }) => ({ id: vote.id })), + q + .from({ lock: locksCollection }) + .select(({ lock }) => ({ id: lock._id })), + ) + .innerJoin({ vote: votesCollection }, ({ id, vote }) => + eq(vote._id, id), + ) + .select(({ id, vote }) => ({ id, lockId: vote.lockId })), + }) + await live.preload() + expect(live.toArray.map((row) => `${row.id}/${row.lockId}`).sort()).toEqual( + [`1/1`, `1/1`, `2/1`, `2/1`, `3/2`], + ) + }) + test(`should allow an include to reuse an alias from a sibling from() subquery`, async () => { const live = createLiveQueryCollection({ startSync: true,