From 9b03e639f769502dccccff7656ba85ab4ca124e8 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Oct 2026 12:21:28 -0600 Subject: [PATCH 1/8] test(db): restore Effect metadata oracle before startup repair --- docs/contributing/oracle-coverage.md | 6 +- .../oracle-reviews/effect-startup-delta.md | 65 ++ packages/db/package.json | 2 +- .../virtual-row-fields-effect-oracle.test.ts | 921 ++++++++++++++++++ 4 files changed, 992 insertions(+), 2 deletions(-) create mode 100644 docs/contributing/oracle-reviews/effect-startup-delta.md create mode 100644 packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index a6616dd4df..3de073768f 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -5774,7 +5774,11 @@ Limits: eager controlled core sources, scalar rows, one mutation per source, and these named paths. No real provider/framework, arbitrary scheduling, ordered repair, same-source overlap, callback-input sanitization or exact internal graph-run callback-count guarantee follows. P09 requires production -repair after the full oracle campaign; this entry makes no closure claim. +repair. The focused Effect follow-up restores the exact accepted owner and its +`test:oracles` entry before production changes; current-main RED, GREEN and +mutant results remain pending. See +[the Effect startup follow-up](oracle-reviews/effect-startup-delta.md). This entry +makes no closure claim. ### RC bound schema mutation receivers (2026-10-28) diff --git a/docs/contributing/oracle-reviews/effect-startup-delta.md b/docs/contributing/oracle-reviews/effect-startup-delta.md new file mode 100644 index 0000000000..7175919d94 --- /dev/null +++ b/docs/contributing/oracle-reviews/effect-startup-delta.md @@ -0,0 +1,65 @@ +# Effect startup delta follow-up + +## Baseline restoration + +This follow-up restores the accepted Effect metadata owner from +`db38b4e009146af53e46aeeef3206269f5968af8`, without editing its model, grammar, +driver, recorder, comparisons, or reach. Its SHA256 is +`4b738962e30f2b0f55b1894bab1f76334da8d66e6001773bbe24599408bb6130`. +The shared source-metadata model and cleanup support already exist on main. +The owner also rejoins the package's explicit `test:oracles` command. + +The baseline is main `bea84cd4554b7bd49df50cb0c569a34d8b2cd915`. Production is +unchanged. The first commit establishes a separately reviewable instrument +before any production repair. No current-main execution result is claimed yet. +Local tests and builds remain unrun under the maintainer's release-session +restriction; current-main reproduction must precede production edits. + +## Law and preserved evidence + +Effect callbacks deliver the query's full before/after delta. An `update` +requires an occurrence that previously entered; a startup-only intermediate +change cannot invent such an occurrence. Explicitly selected source metadata, +previous values, event multiplicity, and active event-key intervals are part of +the comparison. Authority: public `EffectConfig` and `DeltaEvent`, the Effect +guide, `VirtualRowProps`, and the source settlement law. Batch partition is +unconstrained; the check allows empty batches and opaque event keys. + +The reviewed pre-release owner has 200 finite histories across structured and +functional projections, single sources, LEFT joins, and both populated-union +orders. It varies independent optimistic outcomes, confirmations, settlement +order, remote followups, peer exit/reentry, and surviving-row next use. It checks +both batch and named callbacks at every declared action cut, continuing after +a mismatch so startup failure cannot erase later evidence. + +The preserved review records 152 passes and 64 LEFT-join startup REDs, all +5,512 declared lane/channel/cut cells reached, and 2,692 of 2,756 checkpoint +comparisons passing. The failing checkpoint emits an update for a never-entered +unmatched occurrence. Five hostile callback overlays add intended comparison +failures; legal key renaming, fresh reentry keys and empty partitions retain the +baseline. These are historical results, not fresh results on this branch. + +The complete applicable ORC-001 through ORC-016 evaluation, fingerprints, +calibrations, omission attacks and execution commands remain in +[chunk 16 of the accepted campaign record](2026-10-28-pr-2171-oracle-chunks.md#chunk-16-explicit-source-metadata-reaches-effect-deltas-reviewed). +The [release deferral](2026-10-29-pr-2171-deferred-oracles.md) retains the broader +P09–P12 and type-receiver scope. This restoration changes none of those verdicts. + +## Required repair evidence + +Before production changes, run the restored owner on unchanged main and retain +the actual callback mismatch and all per-cell reach. Then run the same owner +after the repair and with the original defect restored as a hostile mutant. +Preserve neighboring single-source/union behavior, both callback channels, +previous values, empty partitions, and later optimistic settlement/next use. + +```sh +TANSTACK_DB_ORACLE_STATISTICS=1 pnpm --filter @tanstack/db exec vitest run \ + tests/query/virtual-row-fields-effect-oracle.test.ts --coverage.enabled=false +``` + +Limits remain eager controlled core sources, scalar projections, one mutation +per source, and the named paths. This supplies no real-provider/framework, +arbitrary scheduling, ordered repair, same-source overlap, or callback-input +sanitization evidence. No GREEN, production repair, or bug-class closure is +claimed by this baseline restoration. diff --git a/packages/db/package.json b/packages/db/package.json index 05e430c107..1b8a299d76 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,7 +23,7 @@ "test": "vitest --run", "test:dist": "vitest run --config vitest.dist.config.ts", "test:facade-retention": "node --expose-gc --import tsx tests/facade-retention.probe.ts", - "test:oracles": "vitest --run --coverage.enabled=false tests/reverse-index-oracle.property.test.ts tests/query/compiler/in-evaluator-oracle.property.test.ts tests/cleanup-queue-oracle.property.test.ts tests/comparison-oracle.property.test.ts tests/duration-query-equality-oracle.test.ts tests/cursor-oracle.property.test.ts tests/index-update-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/change-event-history-oracle.test.ts tests/index-suggestion-oracle.test.ts tests/btree-map-oracle.test.ts tests/paced-mutations-oracle.test.ts tests/db-client-hydration-authority-oracle.test.ts tests/local-storage-order-oracle.test.ts tests/local-storage-value-fidelity-oracle.property.test.ts tests/collection-cleanup-restart-oracle.test.ts tests/collection-gc-transaction-oracle.property.test.ts tests/collection-gc-retention-oracle.property.test.ts tests/collection-gc-mutation-oracle.property.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/transaction-ownership-oracle.property.test.ts tests/optimistic-settlement-boundaries-oracle.test.ts tests/optimistic-history-publication-oracle.test.ts tests/optimistic-history-outcomes-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-index-value-oracle.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history-oracle.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-reentrancy-oracle.test.ts tests/collection-subscription-lifecycle-publication-oracle.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-cache-options-oracle.test.ts tests/mutation-confirmation-oracle.test.ts tests/mutation-handler-types-oracle.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/identity-output-shape-oracle.test.ts tests/query/optimizer-semantics-oracle.test.ts tests/query/index-path-collision-oracle.test.ts tests/query/custom-index-order-selection-oracle.test.ts tests/query/mixed-index-order-oracle.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-alias-shadowing-oracle.test.ts tests/query/includes-outer-join-route-work-oracle.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/subquery-user-value-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state-oracle.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts tests/query/virtual-row-fields-oracle.test.ts tests/query/virtual-row-fields-types-oracle.test.ts tests/query/where-predicate-publication-oracle.property.test.ts tests/query/join-result-key-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/live-query-deferred-acquisition-oracle.test.ts tests/flat-change-tracking-oracle.property.test.ts tests/truncate-readiness-oracle.test.ts tests/proxy-detachment-contract-oracle.test.ts tests/production-error-messages-oracle.test.ts tests/query/bucket-facade-rollback-oracle.property.test.ts tests/query/aggregate-select-oracle.property.test.ts tests/query/load-subset-joint-replay-oracle.property.test.ts", + "test:oracles": "vitest --run --coverage.enabled=false tests/reverse-index-oracle.property.test.ts tests/query/compiler/in-evaluator-oracle.property.test.ts tests/cleanup-queue-oracle.property.test.ts tests/comparison-oracle.property.test.ts tests/duration-query-equality-oracle.test.ts tests/cursor-oracle.property.test.ts tests/index-update-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/change-event-history-oracle.test.ts tests/index-suggestion-oracle.test.ts tests/btree-map-oracle.test.ts tests/paced-mutations-oracle.test.ts tests/db-client-hydration-authority-oracle.test.ts tests/local-storage-order-oracle.test.ts tests/local-storage-value-fidelity-oracle.property.test.ts tests/collection-cleanup-restart-oracle.test.ts tests/collection-gc-transaction-oracle.property.test.ts tests/collection-gc-retention-oracle.property.test.ts tests/collection-gc-mutation-oracle.property.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/transaction-ownership-oracle.property.test.ts tests/optimistic-settlement-boundaries-oracle.test.ts tests/optimistic-history-publication-oracle.test.ts tests/optimistic-history-outcomes-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-index-value-oracle.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history-oracle.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-reentrancy-oracle.test.ts tests/collection-subscription-lifecycle-publication-oracle.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-cache-options-oracle.test.ts tests/mutation-confirmation-oracle.test.ts tests/mutation-handler-types-oracle.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/identity-output-shape-oracle.test.ts tests/query/optimizer-semantics-oracle.test.ts tests/query/index-path-collision-oracle.test.ts tests/query/custom-index-order-selection-oracle.test.ts tests/query/mixed-index-order-oracle.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-alias-shadowing-oracle.test.ts tests/query/includes-outer-join-route-work-oracle.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/subquery-user-value-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state-oracle.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts tests/query/virtual-row-fields-oracle.test.ts tests/query/virtual-row-fields-effect-oracle.test.ts tests/query/virtual-row-fields-types-oracle.test.ts tests/query/where-predicate-publication-oracle.property.test.ts tests/query/join-result-key-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/live-query-deferred-acquisition-oracle.test.ts tests/flat-change-tracking-oracle.property.test.ts tests/truncate-readiness-oracle.test.ts tests/proxy-detachment-contract-oracle.test.ts tests/production-error-messages-oracle.test.ts tests/query/bucket-facade-rollback-oracle.property.test.ts tests/query/aggregate-select-oracle.property.test.ts tests/query/load-subset-joint-replay-oracle.property.test.ts", "bench:nested-includes": "vitest bench tests/query/includes-performance.bench.ts --run", "test:indexed-db:package": "pnpm --filter @tanstack/db... build && vitest run --config vitest.indexed-db.package.config.ts", "test:collection-gc:browser": "playwright test --config playwright.collection-gc.config.ts", diff --git a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts new file mode 100644 index 0000000000..89f670837f --- /dev/null +++ b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts @@ -0,0 +1,921 @@ +/** + * # Do Effects deliver the source metadata a query explicitly selects? + * + * VirtualRowProps permits selecting source metadata. EffectConfig promises the + * query's complete deltas through onBatch and once per row through the named + * handlers; DeltaEvent gives updates the value before the batch. Authority: + * virtual-props.ts, query/effect.ts public types, and live-queries.md's Effect + * query-feature and delta sections. Optimistic-history-oracle supplies the + * source visibility and VirtualOrigin settlement law. + * + * The model authors source rows, optimistic snapshots and applied origins. It + * recomputes ordinary projected fields, then differences two result arrays. + * The actual Effect callbacks supply two independent ledgers. Each ledger must + * contain exactly that delta, including previous values and multiplicity. + * Source metadata is copied into ordinary observed* fields: no output Collection + * exists, so this oracle assigns no implicit Effect-root metadata or ID. + * + * The finite 200 histories cross structured/functional projection, one source, + * a LEFT join with a matched row and unmatched anchor, and populated unions in + * both branch orders. Each source has at most one persisting update. Outcomes, + * queued confirmations and settlement orders vary independently. Later remote + * writes and a peer's exit/reentry check next use and surviving-row isolation. + * Removing a dimension loses that receiving path or settlement distinction. + * Reject-before-update and update-of-absent-key histories are excluded. + * + * Sources are eager and preloaded. At each action cut a host turn drains queued + * delivery, then the whole new prefix of each callback ledger is checked. The + * recorder copies flat scalar fields when invoked, before another action can + * replace them. Earlier ledgers are retained. Each cut collects its mismatches + * without changing the authored next state; the history reports all failures. + * Batch partition, including empty partitions, is unconstrained here; exact + * event multiplicity remains observable in both callback channels. + * A malformed startup delta therefore cannot prevent later comparisons. Reach + * counts exercised cuts; separate counters report which comparisons passed. + * A final drain after disposal checks for extra delta events. This bounded schedule + * does not establish arbitrary provider delays, framework hooks, ordered + * repairs, changing join keys, same-source overlap or subquery sanitization. + * + * Event keys are opaque. Both callback channels must agree; distinct live + * occurrences need distinct keys, and a live occurrence keeps its key through + * updates. An exit retires that correspondence. Reentry may use a fresh key. + * Exact typed source identity is checked through ordinary observedKey instead. + * The four unclaimed Effect-root virtual names are excluded from the ordinary + * field projection, not interpreted as an output-row attribution policy. + */ +import { isDeepStrictEqual } from 'node:util' +import { afterAll, expect, test } from 'vitest' +import { createCollection } from '../../src/collection/index.js' +import { Query, coalesce, eq } from '../../src/query/index.js' +import { createEffect } from '../../src/query/effect.js' +import { declareOracle } from '../oracle-declaration.js' +import { createOracleReach } from '../oracle-reach.js' +import { flushPromises, mockSyncCollectionOptions } from '../utils.js' +import { expectedSourceMetadata } from './virtual-row-fields-source-oracle.js' +import { + checkWithCleanup, + virtualFieldNames, +} from './virtual-row-fields-support.js' +import type { DeltaEvent } from '../../src/query/effect.js' +import type { Transaction } from '../../src/transactions.js' + +type Key = string | number +type Side = `left` | `right` +type Origin = `local` | `remote` +type SourceRow = { + id: Key + parentId: string + side: Side + label: string +} +type SourceState = { + id: string + base: Array + origins: Map + overlay?: SourceRow + confirmation?: SourceRow +} +type Projection = `structured` | `functional` +type Kind = `single` | `join` | `union` +type History = { + kind: Kind + projection: Projection + reverse: boolean + confirm: ReadonlyArray + reject: ReadonlyArray + rightFirst: boolean +} +type ProjectedRow = Record & { id: Key; side: Side } +type ModelEvent = { + type: `enter` | `update` | `exit` + value: ProjectedRow + previousValue?: ProjectedRow +} +type RecordedEvent = { + type: string + key: Key + value: unknown + previousValue?: unknown +} +type NamedEvent = RecordedEvent & { route: ModelEvent[`type`] } +type Ledger = { + batches: Array> + named: Array +} +type KeyIntervals = Map + +function visibleSource(source: SourceState) { + return source.base.map((base) => { + const pending = source.overlay?.id === base.id + const row = pending ? source.overlay! : base + return { + ...row, + ...expectedSourceMetadata( + row.id, + source.id, + pending, + source.origins.get(row.id) ?? `remote`, + ), + } + }) +} +type SourceValue = ReturnType[number] + +// SELECT copies these source facts into ordinary fields. New output objects +// need no root metadata judgment to determine their explicitly selected values. +function projectedSource(row: SourceValue): ProjectedRow { + return { + id: row.id, + side: row.side, + label: row.label, + observedKey: row.$key, + observedCollectionId: row.$collectionId, + observedOrigin: row.$origin, + observedPending: row.$hasPendingWrites, + } +} +function expectedEffectRows( + kind: Kind, + left: SourceState, + right: SourceState, +): Array { + const a = visibleSource(left) + const b = visibleSource(right) + if (kind === `single`) return a.map(projectedSource) + if (kind === `union`) return [...a, ...b].map(projectedSource) + return a.map((row) => { + const joined = b.find((candidate) => candidate.parentId === row.id) + return { + ...projectedSource(row), + rightLabel: joined?.label, + rightKey: joined?.$key, + rightCollectionId: joined?.$collectionId, + rightOrigin: joined?.$origin, + rightPending: joined?.$hasPendingWrites, + } + }) +} +function occurrence(value: unknown): string { + if (!value || typeof value !== `object`) return `invalid:${String(value)}` + const row = value as Record + return `${String(row.side)}:${typeof row.id}:${String(row.id)}` +} +// Independent before/after differencing retains every occurrence. Equality +// distinguishes a missing own field from a field whose value is undefined. +function expectedEffectDelta( + before: ReadonlyArray, + after: ReadonlyArray, +): Array { + const delta: Array = [] + for (const old of before) { + if (!after.some((row) => occurrence(row) === occurrence(old))) + delta.push({ type: `exit`, value: old }) + } + for (const row of after) { + const old = before.find((prior) => occurrence(prior) === occurrence(row)) + if (!old) delta.push({ type: `enter`, value: row }) + else if (!isDeepStrictEqual(old, row)) + delta.push({ type: `update`, value: row, previousValue: old }) + } + return delta +} +function eventOrder(a: { value: unknown }, b: { value: unknown }): number { + return occurrence(a.value).localeCompare(occurrence(b.value)) +} +function eventValue(event: RecordedEvent) { + const { key: _key, ...selected } = event + return selected +} +// Named delivery and batch delivery each refine the independent full delta. +// Cross-channel agreement alone would allow both channels to share a defect. +function checkEffectDelta( + ledger: Ledger, + expected: ReadonlyArray, + active: KeyIntervals, +): void { + const batches = ledger.batches.flat() + const failures: Array = [] + const judge = (law: string, compare: () => void) => { + try { + compare() + } catch (error) { + failures.push(new Error(law, { cause: error })) + } + } + judge(`batch delta and previous values`, () => { + expect(batches.map(eventValue).sort(eventOrder)).toStrictEqual( + [...expected].sort(eventOrder), + ) + }) + judge(`named delta, route and previous values`, () => { + expect(ledger.named.map(eventValue).sort(eventOrder)).toStrictEqual( + expected + .map((event) => ({ ...event, route: event.type })) + .sort(eventOrder), + ) + }) + judge(`named and batch event keys agree`, () => { + expect( + ledger.named.map(({ route: _route, ...event }) => event).sort(eventOrder), + ).toStrictEqual([...batches].sort(eventOrder)) + }) + + // Report an invalid event without adopting it as a live occurrence. Later + // lawful enters in the same batch can still establish their own intervals. + for (const event of batches) { + const id = occurrence(event.value) + judge(`opaque key interval for ${event.type} ${id}`, () => { + expect([`string`, `number`]).toContain(typeof event.key) + if (event.type === `enter`) { + expect(active.has(id), `enter starts an absent occurrence`).toBe(false) + expect( + [...active.values()], + `live occurrence keys are distinct`, + ).not.toContain(event.key) + active.set(id, event.key) + } else { + expect(active.has(id), `update/exit names a live occurrence`).toBe(true) + expect(event.key, `key remains stable within its active interval`).toBe( + active.get(id), + ) + if (event.type === `exit`) active.delete(id) + } + }) + } + if (failures.length) + throw new AggregateError(failures, `Effect delta law failed`, { + cause: failures[0], + }) +} + +// One source has two outcomes and two confirmation states. Two sources have +// four independent outcomes, four confirmation masks and two settlement orders. +// Only unions vary branch order: it has observable source-selection machinery. +const effectMetadataHistories: Array = ( + [`structured`, `functional`] as const +).flatMap((projection) => + ([`single`, `join`, `union`] as const).flatMap((kind) => + (kind === `union` ? [false, true] : [false]).flatMap((reverse) => + (kind === `single` + ? [[], [`left`]] + : [[], [`left`], [`right`], [`left`, `right`]] + ).flatMap((confirm) => + (kind === `single` + ? [[], [`left`]] + : [[], [`left`], [`right`], [`left`, `right`]] + ).flatMap((reject) => + (kind === `single` ? [false] : [false, true]).map((rightFirst) => ({ + kind, + projection, + reverse, + confirm: confirm as Array, + reject: reject as Array, + rightFirst, + })), + ), + ), + ), + ), +) +function pathOf(history: Pick) { + return `${history.kind}${history.kind === `union` ? (history.reverse ? `-right-first` : `-left-first`) : ``}/${history.projection}` +} +function sidesOf(history: History): Array { + return history.kind === `single` ? [`left`] : [`left`, `right`] +} +function historyId(history: History): string { + return `${pathOf(history)}/confirm-${history.confirm.join(`+`) || `none`}/reject-${history.reject.join(`+`) || `none`}/settle-${history.rightFirst ? `right` : `left`}-first` +} +// Declare each finite lane's owed comparisons independently. Aggregate path +// totals would let one omitted right-source action borrow another lane's reach. +// Outcome and confirmation premises determine which cuts are legal in a lane. +function requiredEffectCuts(history: History): Array { + const sides = sidesOf(history) + return [ + `initial`, + ...sides.map((side) => `${side}-pending`), + ...(sides.length === 2 ? [`two-pending`] : []), + ...history.confirm.map((side) => `${side}-accepted-held`), + ...sides.map( + (side) => + `${side}-${history.reject.includes(side) ? `rejected` : `fulfilled`}`, + ), + ...(sides.length === 2 + ? [`one-pending-${history.rightFirst ? `left` : `right`}`] + : []), + ...sides.map((side) => `${side}-remote-followup`), + `peer-exit`, + `peer-reentry`, + `survivor-next-use`, + `final-drain`, + ] +} +const effectMetadataReach = createOracleReach( + `effect explicit source metadata`, + effectMetadataHistories.flatMap((history) => + [`batch`, `named`].flatMap((channel) => + requiredEffectCuts(history).map( + (cut) => `${historyId(history)}/${channel}/${cut}`, + ), + ), + ), + // This exhaustive grammar has no seed/path replay. An unrelated replay + // environment must not exempt its required finite lanes from reach. + { replaying: false }, +) +const effectPassedCuts = new Map() +let enteredEffectHistories = 0 +afterAll(() => { + // A checker-only selector enters no production history. Once any native + // history runs, every finite lane still owes its complete declared reach. + if (enteredEffectHistories > 0) effectMetadataReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `effect metadata reach`, + Object.fromEntries(effectMetadataReach.counts()), + `passed comparisons`, + Object.fromEntries(effectPassedCuts), + ) +}) +declareOracle({ + model: [visibleSource, expectedEffectRows, expectedEffectDelta], + check: [checkEffectDelta], + grammar: [effectMetadataHistories], +}) + +function gate() { + let resolve!: () => void + let reject!: (reason: Error) => void + const promise = new Promise((yes, no) => { + resolve = yes + reject = no + }) + void promise.catch(() => {}) + return { promise, resolve, reject, calls: 0 } +} +function createSource(state: SourceState, hold: ReturnType) { + return createCollection({ + ...mockSyncCollectionOptions({ + id: state.id, + getKey: (row) => row.id, + initialData: state.base.map((row) => ({ ...row })), + }), + onUpdate: async () => { + hold.calls++ + return hold.promise + }, + }) +} +type Source = ReturnType +function captureSelected(value: unknown): unknown { + if (!value || typeof value !== `object`) return value + return Object.fromEntries( + Object.entries(value).filter( + ([name]) => !virtualFieldNames.some((field) => field === name), + ), + ) +} +function captureEvent(event: DeltaEvent): RecordedEvent { + return { + type: event.type, + key: event.key, + value: captureSelected(event.value), + ...(`previousValue` in event + ? { previousValue: captureSelected(event.previousValue) } + : {}), + } +} +// Driver projection: runtime source objects, not model source snapshots. +function selectSource(row: SourceValue) { + return { + id: row.id, + side: row.side, + label: row.label, + observedKey: row.$key, + observedCollectionId: row.$collectionId, + observedOrigin: row.$origin, + observedPending: row.$hasPendingWrites, + } +} +function startEffect( + history: History, + left: Source, + right: Source, + ledger: Ledger, + errors: Array, +) { + return createEffect({ + query: () => { + if (history.kind === `union`) { + const query = new Query().unionAll( + history.reverse ? { b: right, a: left } : { a: left, b: right }, + ) + return history.projection === `functional` + ? query.fn.select(({ a, b }) => { + const row = a ?? b + if (!row) throw new Error(`union has no active source`) + return selectSource(row) + }) + : query.select(({ a, b }) => ({ + id: coalesce(a.id, b.id), + side: coalesce(a.side, b.side), + label: coalesce(a.label, b.label), + observedKey: coalesce(a.$key, b.$key), + observedCollectionId: coalesce(a.$collectionId, b.$collectionId), + observedOrigin: coalesce(a.$origin, b.$origin), + observedPending: coalesce( + a.$hasPendingWrites, + b.$hasPendingWrites, + ), + })) + } + const base = new Query().from({ a: left }) + if (history.kind === `join`) { + const query = base.leftJoin({ b: right }, ({ a, b }) => + eq(a.id, b.parentId), + ) + return history.projection === `functional` + ? query.fn.select(({ a, b }) => ({ + ...selectSource(a), + rightLabel: b?.label, + rightKey: b?.$key, + rightCollectionId: b?.$collectionId, + rightOrigin: b?.$origin, + rightPending: b?.$hasPendingWrites, + })) + : query.select(({ a, b }) => ({ + id: a.id, + side: a.side, + label: a.label, + observedKey: a.$key, + observedCollectionId: a.$collectionId, + observedOrigin: a.$origin, + observedPending: a.$hasPendingWrites, + rightLabel: b.label, + rightKey: b.$key, + rightCollectionId: b.$collectionId, + rightOrigin: b.$origin, + rightPending: b.$hasPendingWrites, + })) + } + return history.projection === `functional` + ? base.fn.select(({ a }) => selectSource(a)) + : base.select(({ a }) => ({ + id: a.id, + side: a.side, + label: a.label, + observedKey: a.$key, + observedCollectionId: a.$collectionId, + observedOrigin: a.$origin, + observedPending: a.$hasPendingWrites, + })) + }, + onBatch: (events) => { + ledger.batches.push(events.map(captureEvent)) + }, + onEnter: (event) => { + ledger.named.push({ route: `enter`, ...captureEvent(event) }) + }, + onUpdate: (event) => { + ledger.named.push({ route: `update`, ...captureEvent(event) }) + }, + onExit: (event) => { + ledger.named.push({ route: `exit`, ...captureEvent(event) }) + }, + onError: (error) => errors.push(error), + onSourceError: (error) => errors.push(error), + }) +} + +test.each(effectMetadataHistories)( + `delivers explicitly selected source metadata through Effects: %j`, + async (history) => { + enteredEffectHistories++ + effectMetadataReach.beginHistory() + const path = pathOf(history) + const id = historyId(history) + const reach = (cut: string) => { + effectMetadataReach.reach(`${id}/batch/${cut}`) + effectMetadataReach.reach(`${id}/named/${cut}`) + } + const leftMain: SourceRow = { + id: history.kind === `union` ? 1 : `a`, + parentId: `unused`, + side: `left`, + label: `left-base`, + } + const peer: SourceRow = { + id: `anchor`, + parentId: `unused`, + side: `left`, + label: `peer`, + } + const rightMain: SourceRow = { + id: history.kind === `union` ? `1` : `r`, + parentId: `a`, + side: `right`, + label: `right-base`, + } + const states: Record = { + left: { + id: `effect-left`, + base: [{ ...leftMain }, { ...peer }], + origins: new Map(), + }, + right: { + id: `effect-right`, + base: [{ ...rightMain }], + origins: new Map(), + }, + } + const holds = { left: gate(), right: gate() } + let left: Source | undefined + let right: Source | undefined + let effect: ReturnType | undefined + const settlements: Partial>> = {} + const transactions = new Map() + const failures = { + left: new Error(`left rejection`), + right: new Error(`right rejection`), + } + const ledger: Ledger = { batches: [], named: [] } + const errors: Array = [] + const mismatches: Array = [] + const active: KeyIntervals = new Map() + let checkedBatches = 0 + let checkedNamed = 0 + let previous: Array = [] + const modelRows = () => + expectedEffectRows(history.kind, states.left, states.right) + const cut = async (name: string, premise?: () => void) => { + await flushPromises() + const next = modelRows() + let premiseReached = true + try { + premise?.() + } catch (error) { + premiseReached = false + mismatches.push( + new Error(`Effect metadata premise ${name}`, { cause: error }), + ) + } + try { + checkEffectDelta( + { + batches: ledger.batches.slice(checkedBatches), + named: ledger.named.slice(checkedNamed), + }, + expectedEffectDelta(previous, next), + active, + ) + expect(errors).toStrictEqual([]) + const key = `${path}/${name}` + effectPassedCuts.set(key, (effectPassedCuts.get(key) ?? 0) + 1) + } catch (error) { + mismatches.push( + new Error(`Effect metadata cut ${name}`, { cause: error }), + ) + } + checkedBatches = ledger.batches.length + checkedNamed = ledger.named.length + previous = next + if (premiseReached) reach(name) + } + const write = ( + side: Side, + type: `insert` | `update` | `delete`, + row: SourceRow, + ) => { + const source = side === `left` ? left! : right! + source.utils.begin() + source.utils.write({ type, value: { ...row } }) + return source.utils.commit() + } + const requirePersisting = (side: Side) => { + expect(holds[side].calls, `${side} handler entered once`).toBe(1) + expect( + transactions.get(side)?.state, + `${side} transaction persists`, + ).toBe(`persisting`) + } + const drive = async () => { + left = createSource(states.left, holds.left) + right = createSource(states.right, holds.right) + await Promise.all([left.preload(), right.preload()]) + effect = startEffect(history, left, right, ledger, errors) + await cut(`initial`) + const sides = sidesOf(history) + for (const side of sides) { + const source = side === `left` ? left : right + const row = states[side].base[0]! + states[side].overlay = { ...row, label: `${side}-pending` } + const transaction = source.update(row.id, (draft) => { + draft.label = `${side}-pending` + }) + transactions.set(side, transaction) + settlements[side] = transaction.when(`settled`).then( + () => undefined, + (error: unknown) => error, + ) + await cut(`${side}-pending`, () => requirePersisting(side)) + } + if (sides.length === 2) + await cut(`two-pending`, () => { + requirePersisting(`left`) + requirePersisting(`right`) + }) + for (const side of history.confirm) { + const row = { ...states[side].base[0]!, label: `${side}-server` } + states[side].confirmation = row + // Commit accepts this row; the persisting overlay still owns visibility. + write(side, `update`, row) + await cut(`${side}-accepted-held`, () => requirePersisting(side)) + } + const order: Array = + history.kind === `single` + ? [`left`] + : history.rightFirst + ? [`right`, `left`] + : [`left`, `right`] + for (const [index, side] of order.entries()) { + const state = states[side] + const reject = history.reject.includes(side) + state.overlay = undefined + if (state.confirmation) { + const confirmation = state.confirmation + state.base = state.base.map((row) => + row.id === confirmation.id ? { ...confirmation } : row, + ) + state.origins.set(confirmation.id, reject ? `remote` : `local`) + state.confirmation = undefined + } + if (reject) holds[side].reject(failures[side]) + else holds[side].resolve() + expect(await settlements[side]).toBe( + reject ? failures[side] : undefined, + ) + await cut(`${side}-${reject ? `rejected` : `fulfilled`}`, () => { + expect(holds[side].calls, `${side} handler entered once`).toBe(1) + expect( + transactions.get(side)?.state, + `${side} transaction settled`, + ).toBe(reject ? `failed` : `completed`) + }) + if (sides.length === 2 && index === 0) { + const stillPending = order[1]! + await cut(`one-pending-${stillPending}`, () => { + requirePersisting(stillPending) + expect(transactions.get(side)?.state).toBe( + reject ? `failed` : `completed`, + ) + }) + } + } + for (const side of sides) { + const state = states[side] + const row = { ...state.base[0]!, label: `${side}-later` } + state.base[0] = row + state.origins.set(row.id, `remote`) + write(side, `update`, row) + await cut(`${side}-remote-followup`) + } + states.left.base = states.left.base.filter((row) => row.id !== peer.id) + write(`left`, `delete`, peer) + await cut(`peer-exit`) + states.left.base.push({ ...peer }) + write(`left`, `insert`, peer) + await cut(`peer-reentry`) + const survivor = { ...states.left.base[0]!, label: `survivor-next-use` } + states.left.base[0] = survivor + write(`left`, `update`, survivor) + await cut(`survivor-next-use`) + await effect.dispose() + await cut(`final-drain`) + } + await checkWithCleanup(async () => { + try { + await drive() + } catch (error) { + mismatches.push( + new Error(`Effect metadata driver ${id}`, { cause: error }), + ) + } + if (mismatches.length) + throw new AggregateError(mismatches, `Effect metadata history failed`, { + cause: mismatches[0], + }) + }, [ + async () => { + holds.left.resolve() + holds.right.resolve() + await Promise.all(Object.values(settlements)) + }, + async () => { + await effect?.dispose() + }, + async () => { + await right?.cleanup() + }, + async () => { + await left?.cleanup() + }, + ]) + }, +) + +// The same delta judgment must reject wrong callback histories even when their +// final row is right. Controls seed active key intervals through initial enter +// delivery, so failures reach the update comparison rather than setup. +const effectFaults = [ + `omitted-peer`, + `duplicate-message`, + `duplicate-batch`, + `stale-pending`, + `wrong-source`, + `new-previous-value`, + `extra-undefined`, + `wrong-route`, + `missing-named`, + `delayed-extra`, + `key-collision`, + `key-change`, +] as const +declareOracle({ grammar: [effectFaults] }) +test.each(effectFaults)( + `rejects a hostile Effect metadata ledger: %s`, + (fault) => { + const row = (id: Key, pending: boolean): ProjectedRow => ({ + id, + side: `left`, + label: pending ? `pending` : `base`, + observedKey: id, + observedCollectionId: `source`, + observedOrigin: pending ? `local` : `remote`, + observedPending: pending, + }) + const before = [row(1, false), row(`1`, false)] + const after = [row(1, true), row(`1`, true)] + const events = (expected: Array, prefix: string): Ledger => { + const batch = expected.map((event) => ({ + ...structuredClone(event), + key: `${prefix}/${occurrence(event.value)}`, + })) + return { + batches: [batch], + named: batch.map((event) => ({ + ...structuredClone(event), + route: event.type, + })), + } + } + const active: KeyIntervals = new Map() + checkEffectDelta( + events(expectedEffectDelta([], before), `opaque`), + expectedEffectDelta([], before), + active, + ) + const expected = expectedEffectDelta(before, after) + const allowed = events(expected, `opaque`) + checkEffectDelta(structuredClone(allowed), expected, new Map(active)) + const hostile = structuredClone(allowed) + const first = hostile.batches[0]![0]! + const second = hostile.batches[0]![1]! + if (fault === `omitted-peer`) hostile.batches[0]!.pop() + if (fault === `duplicate-message`) + hostile.batches[0]!.push(structuredClone(first)) + if (fault === `duplicate-batch`) + hostile.batches.push(structuredClone(hostile.batches[0]!)) + if (fault === `stale-pending`) + (first.value as Record).observedPending = false + if (fault === `wrong-source`) + (first.value as Record).observedCollectionId = + `other-source` + if (fault === `new-previous-value`) + first.previousValue = structuredClone(first.value) + if (fault === `extra-undefined`) + (first.value as Record).extra = undefined + if (fault === `wrong-route`) hostile.named[0]!.route = `enter` + if (fault === `missing-named`) hostile.named.pop() + if (fault === `delayed-extra`) + hostile.batches.push([structuredClone(first)]) + if (fault === `key-collision`) second.key = first.key + if (fault === `key-change`) first.key = `different-opaque-key` + expect(() => checkEffectDelta(hostile, expected, new Map(active))).toThrow() + }, +) + +// Allowed controls preserve implementation freedom: row order and opaque key +// representation do not define metadata, and an exited interval owns no key. +function calibrationRows(pending: boolean): Array { + const base: Array = [ + { id: 1, side: `left`, parentId: `unused`, label: `base` }, + { id: `1`, side: `left`, parentId: `unused`, label: `peer` }, + ] + return expectedEffectRows( + `single`, + { + id: `source`, + base, + origins: new Map(), + ...(pending ? { overlay: { ...base[0]!, label: `pending` } } : {}), + }, + { id: `unused`, base: [], origins: new Map() }, + ) +} +function calibrationLedger( + delta: ReadonlyArray, + keyFor: (value: ProjectedRow) => Key, +): Ledger { + const batch = delta.map((event) => ({ + ...structuredClone(event), + key: keyFor(event.value), + })) + return { + batches: batch.length ? [batch.reverse()] : [], + named: batch.map((event) => ({ + ...structuredClone(event), + route: event.type, + })), + } +} +test(`permits reordered delivery and fresh opaque keys after an exit`, () => { + const before = calibrationRows(false) + const after = calibrationRows(true) + const active: KeyIntervals = new Map() + const oldKey = (row: ProjectedRow) => `arbitrary/${occurrence(row)}` + const initial = expectedEffectDelta([], before) + checkEffectDelta(calibrationLedger(initial, oldKey), initial, active) + const update = expectedEffectDelta(before, after) + checkEffectDelta(calibrationLedger(update, oldKey), update, active) + const remaining = [after[0]!] + const exit = expectedEffectDelta(after, remaining) + checkEffectDelta(calibrationLedger(exit, oldKey), exit, active) + const reentry = expectedEffectDelta(remaining, after) + checkEffectDelta( + calibrationLedger(reentry, (row) => `fresh/${occurrence(row)}`), + reentry, + active, + ) + const next = [before[0]!, after[1]!] + const survivor = expectedEffectDelta(after, next) + checkEffectDelta(calibrationLedger(survivor, oldKey), survivor, active) +}) +test(`permits empty callback partitions while preserving exact delta events`, () => { + const before = calibrationRows(false) + const after = calibrationRows(true) + const active: KeyIntervals = new Map() + const keyFor = (row: ProjectedRow) => occurrence(row) + for (const delta of [ + expectedEffectDelta([], before), + expectedEffectDelta(before, after), + expectedEffectDelta(after, after), + ]) { + const ledger = calibrationLedger(delta, keyFor) + ledger.batches.unshift([]) + ledger.batches.push([]) + checkEffectDelta(ledger, delta, active) + } +}) +test(`records scalar callback values before later object mutation`, () => { + const before = calibrationRows(false) + const after = calibrationRows(true) + const keyFor = (row: ProjectedRow) => occurrence(row) + const active: KeyIntervals = new Map() + const initial = expectedEffectDelta([], before) + checkEffectDelta(calibrationLedger(initial, keyFor), initial, active) + const expected = expectedEffectDelta(before, after) + const row = structuredClone(expected[0]!.value) + const previousValue = structuredClone(expected[0]!.previousValue!) + const event: DeltaEvent = { + type: `update`, + key: keyFor(row), + value: row, + previousValue, + } + const ledger: Ledger = { + batches: [[captureEvent(event)]], + named: [{ route: `update`, ...captureEvent(event) }], + } + row.observedPending = false + previousValue.label = `mutated after delivery` + checkEffectDelta(ledger, expected, active) +}) +test(`preserves later interval judgments after an invalid unknown update`, () => { + const before = calibrationRows(false) + const after = calibrationRows(true) + const keyFor = (row: ProjectedRow) => occurrence(row) + const active: KeyIntervals = new Map() + const initial = expectedEffectDelta([], before) + const ledger = calibrationLedger(initial, keyFor) + const invalid = { + type: `update`, + key: `never-entered`, + value: structuredClone(before[0]!), + previousValue: structuredClone(before[0]!), + } + ledger.batches[0]!.unshift(invalid) + ledger.named.unshift({ ...structuredClone(invalid), route: `update` }) + expect(() => checkEffectDelta(ledger, initial, active)).toThrow() + const update = expectedEffectDelta(before, after) + checkEffectDelta(calibrationLedger(update, keyFor), update, active) +}) From a7ea76cddf7ab2fe03f49d84c6d596dd598fedba Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Oct 2026 12:43:03 -0600 Subject: [PATCH 2/8] fix(db): suppress phantom LEFT-join Effect startup updates --- .changeset/fix-effect-left-join-startup.md | 5 + docs/contributing/oracle-coverage.md | 27 ++- .../2026-10-29-pr-2171-deferred-oracles.md | 11 ++ .../oracle-reviews/effect-startup-delta.md | 154 +++++++++++++++++- packages/db/src/query/effect.ts | 5 +- 5 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 .changeset/fix-effect-left-join-startup.md diff --git a/.changeset/fix-effect-left-join-startup.md b/.changeset/fix-effect-left-join-startup.md new file mode 100644 index 0000000000..7ca7f3ec13 --- /dev/null +++ b/.changeset/fix-effect-left-join-startup.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Prevent phantom Effect update callbacks for unchanged intermediate LEFT-join rows during startup. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 3de073768f..e381058f71 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -5759,9 +5759,10 @@ Held mutations, independent confirmations/outcomes, both settlement orders, remote updates, peer exit/reentry and next-use reach 5,512 lane/channel/cut cells exactly once. No implicit Effect-root metadata or output Collection ID is claimed. -Open P09: all 64 LEFT-join histories emit a startup update for a never-entered -unmatched occurrence after its load finished. Those initial comparisons remain -RED on unchanged `858b174a6`; histories continue and all later comparisons pass. +Original P09 evidence: all 64 LEFT-join histories emitted a startup update for +a never-entered unmatched occurrence after its load finished. Those initial +comparisons were RED on unchanged `858b174a6`; histories continued and all later +comparisons passed. The other 136 native histories and 16 controls pass. Independent hostile overlays reject dispatch, previous-value, duplication, colliding-key and extra undefined defects; lawful key renaming, fresh reentry and empty partitions retain @@ -5773,12 +5774,20 @@ in the RC chunk record for exact fingerprints, evidence and review. Limits: eager controlled core sources, scalar rows, one mutation per source, and these named paths. No real provider/framework, arbitrary scheduling, ordered repair, same-source overlap, callback-input sanitization or exact -internal graph-run callback-count guarantee follows. P09 requires production -repair. The focused Effect follow-up restores the exact accepted owner and its -`test:oracles` entry before production changes; current-main RED, GREEN and -mutant results remain pending. See -[the Effect startup follow-up](oracle-reviews/effect-startup-delta.md). This entry -makes no closure claim. +internal graph-run callback-count guarantee follows. + +P09 is repaired in PR #2301 within this finite controlled-core boundary. +The exact owner and `test:oracles` entry were restored before production edits. +On baseline `9b03e639f`, the fresh RED reproduced 64 startup failures with every +reach cell at count 1. Repaired production passes all 216 owner tests and all +2,756 checkpoint comparisons. Restoring the original classifier reproduces the +same 64 assertion failures; suppressing all unordered updates fails all 200 +native histories. Each of the 5,512 reach cells remains at count 1 in both +mutants and GREEN. All 1,690 neighboring tests pass before and after the edit. +See [the Effect startup follow-up](oracle-reviews/effect-startup-delta.md) for +commands, fingerprints and limits. Arbitrary scheduling, join multiplicity, +ordered repair, same-source overlap and callback-input sanitization remain +outside this bounded closure. Other deferred findings remain open. ### RC bound schema mutation receivers (2026-10-28) diff --git a/docs/contributing/oracle-reviews/2026-10-29-pr-2171-deferred-oracles.md b/docs/contributing/oracle-reviews/2026-10-29-pr-2171-deferred-oracles.md index fa6afe5e13..c92603d698 100644 --- a/docs/contributing/oracle-reviews/2026-10-29-pr-2171-deferred-oracles.md +++ b/docs/contributing/oracle-reviews/2026-10-29-pr-2171-deferred-oracles.md @@ -249,3 +249,14 @@ settlement and after it. The original oracle remains active and unchanged; there is no skip, narrowed comparison, retry-away, GREEN, or bug-class closure claim. Its CI RED remains an open baseline defect under this explicit release scope deferral. + +## P09 follow-up (2026-10-29) + +[PR #2301](https://github.com/TanStack/db/pull/2301) restores the exact accepted +Effect owner and repairs its LEFT-join startup delta. Fresh unchanged-production +RED precedes the production edit. The owner passes all 216 tests after repair, +with every one of its 5,512 reach cells at count 1. Original and overcorrection +mutants fail the shared comparisons. The +[versioned follow-up](effect-startup-delta.md) records commands, fingerprints, +neighboring guarantees and the bounded closure. This updates P09 only. All +other deferrals and limits in this release record remain open. diff --git a/docs/contributing/oracle-reviews/effect-startup-delta.md b/docs/contributing/oracle-reviews/effect-startup-delta.md index 7175919d94..8627e3b795 100644 --- a/docs/contributing/oracle-reviews/effect-startup-delta.md +++ b/docs/contributing/oracle-reviews/effect-startup-delta.md @@ -9,11 +9,12 @@ driver, recorder, comparisons, or reach. Its SHA256 is The shared source-metadata model and cleanup support already exist on main. The owner also rejoins the package's explicit `test:oracles` command. -The baseline is main `bea84cd4554b7bd49df50cb0c569a34d8b2cd915`. Production is -unchanged. The first commit establishes a separately reviewable instrument -before any production repair. No current-main execution result is claimed yet. -Local tests and builds remain unrun under the maintainer's release-session -restriction; current-main reproduction must precede production edits. +The baseline is main `bea84cd4554b7bd49df50cb0c569a34d8b2cd915`. Production was +unchanged in baseline restoration commit `9b03e639f769502dccccff7656ba85ab4ca124e8`. +That commit establishes a separately reviewable instrument before the repair. +No execution result was claimed at restoration. The maintainer subsequently +authorized targeted oracle tests, while retaining the prohibition on full builds. +The dated evidence below supersedes the pending execution status. ## Law and preserved evidence @@ -45,7 +46,7 @@ calibrations, omission attacks and execution commands remain in The [release deferral](2026-10-29-pr-2171-deferred-oracles.md) retains the broader P09–P12 and type-receiver scope. This restoration changes none of those verdicts. -## Required repair evidence +## Baseline repair plan (completed below) Before production changes, run the restored owner on unchanged main and retain the actual callback mismatch and all per-cell reach. Then run the same owner @@ -63,3 +64,144 @@ per source, and the named paths. This supplies no real-provider/framework, arbitrary scheduling, ordered repair, same-source overlap, or callback-input sanitization evidence. No GREEN, production repair, or bug-class closure is claimed by this baseline restoration. + +## Executed repair evidence (2026-10-29) + +Review identity: baseline restoration `9b03e639f769502dccccff7656ba85ab4ca124e8`, +with production still identical to main +`bea84cd4554b7bd49df50cb0c569a34d8b2cd915`. The repaired +`packages/db/src/query/effect.ts` has SHA256 +`b9ea784afe07e156507079224885c73bd21e8633116c3344af809c09971b0879`. +The owner retains the restoration fingerprint above. No model, history, +recorder, comparison, reach requirement, or run budget changed after RED. +This record accompanies the production repair in PR #2301. + +### Law, diagnosis, and repair + +An Effect update describes a changed row that stays in the result. For a +preloaded LEFT join with a matched row and an unmatched anchor, startup owes +only the two entering occurrences. It owes no update for an intermediate +unmatched occurrence that never entered. The public Effect guide's delta +sections and `DeltaEvent.previousValue` establish this judgment independently +of the graph output. + +The original classifier treated any insert plus delete as an unordered update. +Startup emits and retracts an intermediate unmatched row within one graph run. +Its inserted and deleted scalar projections are equal. The callback nevertheless +received that row as an update, followed by the two legitimate enters. +The update's previous and current values were identical, with all right fields +undefined. The independent before/after model rejected the extra event and the +absence of its prior enter in both callback channels. + +The repair applies the existing `deepEquals` update condition to unordered +Effects too. Ordered Effects already use this condition. It preserves genuine +changed-value updates without retaining every unordered result row. Equality +uses the existing change-event equality domain, not D2 hash identity. +This evidence checks scalar projections, not every value in either domain. + +The earlier Effect examples lacked this exact startup delta comparison. The +restored owner checks the complete emitted delta and active key intervals, +rather than inferring correctness from final rows or selected event counts. +No missing-input hypothesis remains for this witness: both source Collections +finish preload before Effect creation, and the correct matched and anchor +enters arrive in the same startup ledger as the extra update. + +### Results + +The pre-edit inventory comprises structured and functional projections through +actual core `createEffect`, with onBatch and named handlers. Single sources, +LEFT joins and both populated-union orders receive the same source model. +There is no real-provider or framework claim requiring another receiving host. +The unchanged-production neighboring run preceded the production edit. + +| Run | Passed / failed tests | Declared reach | Passing checkpoint comparisons | +| --- | --- | --- | --- | +| Unchanged production RED | 152 / 64 | All 5,512 cells: count 1 each | 2,692 / 2,756 | +| Repaired owner GREEN | 216 / 0 | All 5,512 cells: count 1 each | 2,756 / 2,756 | +| Original production restored as mutant | 152 / 64 | All 5,512 cells: count 1 each | 2,692 / 2,756 | +| Suppress all unordered updates mutant | 16 / 200 | All 5,512 cells: count 1 each | Later changed-value comparisons fail | +| Final restored repair plus neighboring suites | 1,906 / 0 | Effect cells unchanged: count 1 each | 2,756 / 2,756 Effect comparisons | + +The 200 native histories comprise 8 single-source, 64 LEFT-join and 128 union +histories. The other 16 tests calibrate the shared judgment. Both structured and +functional LEFT-join startup cuts fail in RED. Every later cut continues and +passes, so startup failure does not hide settlement or next-use evidence. +The original mutant restores the entire production file byte-for-byte to the +baseline and reproduces the same startup assertion failures. The second mutant +requires `publishedRows` for updates, wrongly dropping all unordered updates. +It fails the intended changed-value comparisons in all 200 native histories. +Both outcomes are assertion failures at the shared comparison, not setup errors +or timeouts. Neither mutant remains in the source. + +Neighboring suites pass 1,690 tests both before and after the production edit: +`effect.test.ts` (74), `effect-disposal-oracle.test.ts` (12), and +`query/virtual-row-fields-oracle.test.ts` (1,604). These retain skipInitial, +previous values, object-valued Set updates, source failure, ordered Effects, +disposal and root metadata observations within their existing bounds. +The baseline remote DB-1 job also fails the same 64 startup cases: +[run 40930211146](https://github.com/TanStack/db/actions/runs/40930211146/job/123646325441). + +All local tests use Node 24.19.0 and installed Vitest 3.2.4 on macOS arm64. +The manifest requests Vitest 4.1.12. No dependencies or lockfiles changed. +Full builds remain unrun by maintainer instruction. + +Commands run from `packages/db` using the worktree's installed binary: + +```sh +TANSTACK_DB_ORACLE_STATISTICS=1 ../../node_modules/.bin/vitest run \ + --coverage.enabled=false tests/query/virtual-row-fields-effect-oracle.test.ts + +../../node_modules/.bin/vitest run --coverage.enabled=false \ + tests/effect.test.ts tests/effect-disposal-oracle.test.ts \ + tests/query/virtual-row-fields-oracle.test.ts + +TANSTACK_DB_ORACLE_STATISTICS=1 ../../node_modules/.bin/vitest run \ + --coverage.enabled=false tests/query/virtual-row-fields-effect-oracle.test.ts \ + tests/effect.test.ts tests/effect-disposal-oracle.test.ts \ + tests/query/virtual-row-fields-oracle.test.ts +``` + +Each executed command also uses default and JSON reporters. Full logs and JSON +reports remain in `/private/tmp/effect-startup-delta/` under `baseline-red`, +`baseline-neighbors`, `repaired-green`, `repaired-neighbors`, `original-mutant`, +`overcorrection-mutant`, and `final-green`. The finite owner reports every named +reach cell, so the table's count 1 applies to each lane/channel/cut separately. +It does not aggregate away a missing path. No random seed applies to this +exhaustive finite grammar. Running the whole owner reconstructs every witness. + +### Self-review against the oracle rubric + +The accepted chunk-16 audit remains applicable to the unchanged owner. This +follow-up adds executed evidence on the restored baseline and repaired source. +The review here is a self-review, not a fresh independent review. + +| Requirement | Follow-up outcome | +| --- | --- | +| ORC-001 | Public delta law and bounded closure are stated above. | +| ORC-002 | Independent source projection and array differencing remain unchanged. No production classifier computes expected deltas. | +| ORC-003 | Restored literate owner keeps contract, model, grammar, driver and comparison together. | +| ORC-004 | All 200 finite histories run. Accepted reconstruction, ablation, range and exclusion controls remain unchanged. | +| ORC-005 | Actual core Effect callbacks supply both ledgers at every action cut. Preloaded inputs and complete startup output resolve the concrete input-sufficiency question. | +| ORC-006 | Original and overcorrection mutants fail the intended comparisons. All 16 checker controls pass on repaired production. | +| ORC-007 | Not triggered by a random property. This grammar exhausts its finite domain and has direct reconstruction. | +| ORC-008 | No reference-model state changes. Existing source visibility and active key intervals retain their accepted distinguishing actions. | +| ORC-009 | Existing source rows, optimistic state, source origins and active event-key intervals retain their documented mapping. | +| ORC-010 | Primary and cleanup errors remain distinguishable through the shared cleanup wrapper. The owner continues after mismatches and drains after disposal. | +| ORC-011 | No new concrete shared semantic fault requires a second formulation. The matched/unmatched startup result follows directly from the public delta contract. | +| ORC-012 | This versioned record identifies baseline and repaired source fingerprints, results, limits and every requirement. | +| ORC-013 | Equal intermediate startup values distinguish suppression. Later changed values, both settlements and next use reject suppress-all-updates. | +| ORC-014 | No real-provider or framework handoff claim. Sources are controlled core Collections. | +| ORC-015 | Every one of the 5,512 declared cells reaches count 1 in RED, GREEN and both mutant runs. | +| ORC-016 | The restored owner contains shared model/driver comparisons and checker calibrations. No independent expected-answer regression was added. | + +### Closure and remaining limits + +P09 is repaired within this contract × history × production-path × observation +boundary: exact scalar deltas for eager preloaded core Effects, the 200 stated +histories, both projection forms and both callback channels, at every declared +action cut. No known counterexample remains inside those exercised cells. +This does not establish arbitrary join multiplicity or scheduling, changing join +keys, same-source overlap, ordered repair, real providers/frameworks, or +callback-input sanitization. P10, P11/P12 and the other deferred runtime, +retention and public-type findings remain open under their existing owners. +No broader metadata or general delta-classifier closure follows. diff --git a/packages/db/src/query/effect.ts b/packages/db/src/query/effect.ts index 5629064a7f..bd777b10d8 100644 --- a/packages/db/src/query/effect.ts +++ b/packages/db/src/query/effect.ts @@ -1347,9 +1347,10 @@ function classifyDelta( wasPresent && isPresent && insertValue !== undefined && - (!publishedRows || !deepEquals(previousValue, insertValue)) + !deepEquals(previousValue, insertValue) ) { - // Row updated within the query result + // Equal insert/delete values can cancel an intermediate LEFT-join row + // within this graph run. Only a changed final value is an update. return { type: `update`, key, From 1cfd9d82b31bc192c7af631200c3802f9a66acbd Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Oct 2026 14:22:50 -0600 Subject: [PATCH 3/8] Fix Set comparison work and isolate key-continuity calibration Law: Preserve change-event Set assignment equality from the glossary and deepEquals API; remove factorial flat-member work under AGENTS.md. ORC-006 requires calibration of the intended key-continuity judgment. Reach: Eight direct states reached 200 times each; 180 receiver cells reached once each; all 5,512 scalar startup cells still reached once. RED: On unchanged a7ea76c production, the extended equality/receiver command recorded in docs/contributing/oracle-reviews/set-equality-work.md passed 134 tests and failed 11 work assertions. Old key controls accepted the mutant. GREEN: The documented seven-suite command passed 619 tests with zero skipped. Targeted TypeScript, ESLint, oracle lint and formatting passed. Mutant: Original matcher failed 11 work assertions; greedy matcher failed two equality assertions. Original replay seed -310527886 path 0:0 failed once. Deleting key stability from the full owner failed exactly two controls. Limits: Flat work sizes 1/2/4/6/8, homologous two-member class/cycle shapes, and eager controlled core receivers. No arbitrary graph complexity or provider/framework claim. Key controls isolate update continuity only. --- docs/contributing/oracle-coverage.md | 20 ++ .../oracle-reviews/effect-startup-delta.md | 49 ++++ .../oracle-reviews/set-equality-work.md | 166 ++++++++++++ packages/db/package.json | 2 +- packages/db/src/utils.ts | 60 ++--- packages/db/tests/oracle-config.ts | 2 +- .../set-equality-receivers-oracle.test.ts | 240 ++++++++++++++++++ .../virtual-row-fields-effect-oracle.test.ts | 12 +- packages/db/tests/set-equality-oracle.ts | 186 ++++++++++++++ .../db/tests/utils-oracle.property.test.ts | 32 ++- 10 files changed, 735 insertions(+), 34 deletions(-) create mode 100644 docs/contributing/oracle-reviews/set-equality-work.md create mode 100644 packages/db/tests/query/set-equality-receivers-oracle.test.ts create mode 100644 packages/db/tests/set-equality-oracle.ts diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index e381058f71..b73481bd86 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -1889,6 +1889,17 @@ The [round 7 ledger](oracle-reviews/utils-unit-retirement-round-7.md) retains the retired examples. Caller-modified built-ins and Collection publication are outside this direct-return owner. +The [Set-work extension](oracle-reviews/set-equality-work.md) adds `utils.41` +and a shared descriptor judgment in `set-equality-oracle.ts`. Class/plain +assignment and homologous cycles preserve equality while flat duplicate-valued +members require quadratic leaf reads. Both 100-run campaigns reach all eight +states 200 times together. `query/set-equality-receivers-oracle.test.ts` reuses +the judgment for Collection changes and ordered/unordered Effects, including +scalar sources that synthesize projected Sets. All 180 finite receiving cells +reach count 1. Original-matcher and greedy mutants fail work and equality +assertions respectively. This bounds the stated flat-member family, not nested +graph complexity, changing getters, arbitrary graphs or every comparator caller. + The [Duration query equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/duration-query-equality-oracle.test.ts) uses ten public fields to predict direct BasicIndex/BTreeIndex exact lookups and public `eq` snapshots before and after indexing. The `in` evaluator owner also @@ -5789,6 +5800,15 @@ commands, fingerprints and limits. Arbitrary scheduling, join multiplicity, ordered repair, same-source overlap and callback-input sanitization remain outside this bounded closure. Other deferred findings remain open. +Subsequent checker calibration keeps both callback channels coherent when +injecting a bad update key. The `key-collision` and `key-change` controls now +reject deletion of the interval-continuity judgment itself: the full checker +mutant passes 214 controls/histories and fails those two controls. Repaired +production passes all 216 owner tests and the combined 619 targeted tests. +Native histories and all 5,512 reach cells are unchanged. See the follow-up's +[calibration evidence](oracle-reviews/effect-startup-delta.md#key-interval-checker-calibration) +for the new owner fingerprint and the limits of the continuity claim. + ### RC bound schema mutation receivers (2026-10-28) Owner: `packages/db/tests/live-query-bound-options-oracle.ts`; shared driver: diff --git a/docs/contributing/oracle-reviews/effect-startup-delta.md b/docs/contributing/oracle-reviews/effect-startup-delta.md index 8627e3b795..dfb3b6c4ab 100644 --- a/docs/contributing/oracle-reviews/effect-startup-delta.md +++ b/docs/contributing/oracle-reviews/effect-startup-delta.md @@ -205,3 +205,52 @@ keys, same-source overlap, ordered repair, real providers/frameworks, or callback-input sanitization. P10, P11/P12 and the other deferred runtime, retention and public-type findings remain open under their existing owners. No broader metadata or general delta-classifier closure follows. + +## Subsequent prep-pr review + +Review of `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` found a newly exposed +factorial object-Set comparison path. The [Set-work repair](set-equality-work.md) +records its independent model, pre-edit RED, receiving paths, GREEN and mutants. +The scalar startup owner's native model and histories still pass all 216 cases. +Its separate opaque-key calibration repair is recorded below; startup and work +evidence alone would not close that test-integrity gap. + +### Key-interval checker calibration + +CodeRabbit review `5703351893` on `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` +identified a compound-fault calibration. The `key-collision` and `key-change` +controls changed only batch keys. Named callbacks retained their original keys, +so cross-channel disagreement rejected the ledger even without the interval +judgment. The checker still enforced continuity; these controls did not isolate +it. ORC-006 requires sensitivity at the intended judgment. + +The maintainer accepted this repair. Both controls now put the same bad key in +the matching named entry. `events()` constructs named entries directly from the +batch, preserving their order. No production behavior, independent model, +native history, assertion, exclusion, run budget or reach requirement changed. + +Before repair, deleting the interval judgment leaves both controls passing: +the checker mutant survives. After coherent mutation, that deletion causes +exactly two expected-to-throw assertion failures. Removing only the key-stability +assertion from the full repaired owner gives **214 passes and those same two +failures**, with zero skipped tests. Cross-channel agreement therefore cannot +substitute for continuity enforcement. + +The repaired owner passes all **216 tests**. Combined equality, Collection/Effect +receiving, proxy-revert and startup checks pass **619/619**. All 5,512 startup +reach cells still count 1; Set-work reach is unchanged. Targeted TypeScript, +ESLint, oracle lint and formatting pass. Tests use at most two workers; no full +build ran. Other applicable rubric outcomes above remain unchanged. + +This is a test-integrity repair with a checker mutant, not a new production RED. +These update faults calibrate continuity for a live occurrence. They do not +independently mutate the distinct-key condition on a fresh enter, or establish +arbitrary graph, provider, framework or rich named-callback coverage. + +The restoration and execution fingerprints above remain historical evidence. +The current owner intentionally changes only key-fault setup and its explanation. +Its SHA256 is `56de9e93892c6a4772f71846e25fc9e3700359b2cadbe3d6d9b359d7b9bcd46f`. +Review identity is parent `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` plus that +fingerprint and the Set-work fingerprints. The combined command is recorded in +the Set-work review. Raw checker variants, reports and logs remain under +`/private/tmp/effect-startup-delta/prep-pr/key-calibration/accepted-cr1/`. diff --git a/docs/contributing/oracle-reviews/set-equality-work.md b/docs/contributing/oracle-reviews/set-equality-work.md new file mode 100644 index 0000000000..21a6a09209 --- /dev/null +++ b/docs/contributing/oracle-reviews/set-equality-work.md @@ -0,0 +1,166 @@ +# Object-Set equality work in Effect publication + +## Law and repair boundary + +PR #2301 makes unordered Effects compare old and new projected values. Review +of head `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` found that the existing +object-Set matcher retries permutations when several members have equal contents +and another member differs. A scalar source row can project those Sets, so this +new Effect cost does not already occur in source-Collection comparison. + +Preserve change-event equality: Set order does not matter, each member matches +once, and distinct objects with equal contents retain their multiplicity. +The glossary and `deepEquals` API supply the authority. A keyed class matches +a plain record with equal fields, but two different classes differ. Matching +must permit reassignment; greedy pairing changes the accepted law. + +The maintainer accepted this performance repair after independent review. +Its engineering requirement removes factorial assignment work under AGENTS.md's +algorithm-efficiency guidance, without adding a numerical latency API promise. +Flat one-field members permit at most `2*n*m` leaf reads per direct comparison. +Each core one-row replacement permits `16*(n+1)^2` reads, allowing repeated +comparisons at fixed bookkeeping boundaries. Both bounds were stated before +changing production. They do not promise quadratic total runtime or bound +arbitrary graph complexity. + +## Instrument and pre-edit gate + +The primary `utils-oracle.property.test.ts` owner imports the descriptor judgment +in `set-equality-oracle.ts`. That model enumerates compatible assignments with +memoized subsets. It neither reads production objects nor uses production +equality. Production instead finds augmenting paths through cached candidates. +Each candidate starts with the same active ancestors; recursive comparisons +remove their own pairs before returning. Another member's assignment therefore +does not change a cached candidate's context. + +The generated family exhausts ordered two-member words over plain records and +two keyed classes in five homologous shapes: flat, self-cycle, owner-cycle, +nested owner-cycle, and shared acyclic leaf. Payloads range from -100 to 100. +Right-word reversal repeats already exhausted permutations; it adds no claimed +class case. Flat duplicate families use sizes 1, 2, 4, 6 and 8 with equal copies, +one late changed leaf, and different cardinality. Removing duplicate values +loses the performance witness; removing class/plain overlap loses the greedy +counterexample; removing ancestor shapes loses cycle-context evidence. +Members are distinct objects. Repeated references would collapse in a real Set +and are excluded from this multiplicity grammar. + +The appended property is `utils.41`; selectors 1–40 stay unchanged. Fixed seed +20260911 and a fresh seed each run 100 instances of the same property and budget. +The extension `query/set-equality-receivers-oracle.test.ts` reuses that equality +judgment for 20 finite histories on each of three actual core paths: Collection +change events, unordered Effects and ordered Effects. Effect sources contain +only scalar `id` and `tick` fields; their projection constructs the Set. +Equal, reordered, late-mismatch and changed-multiplicity replacements are checked +at startup, replacement and next use. Event multiplicity, selected member values, +row ID and previous values remain observable. + +Before production edits, the extended owners had **134 passes and 11 work +assertion failures**: two generated campaigns and nine receiving histories. +Returned equality and public deltas remained correct. Four flat members needed +128 reads against a bound of 32. At eight members each late-mismatch receiver +needed 219,200 reads against 1,296. The neighboring utils, proxy-revert, Effect +and startup suites passed **474/474**, including all 216 startup-owner tests, +before the matcher changed. + +An initial run exposed a missing replay-registry entry, then registration order +shifted existing selectors. Both instrument issues were repaired before final +RED; neither setup failure is product evidence. Two test-only generic/union +annotations were also corrected after targeted type checking. + +## GREEN, calibration, and replay + +The matcher caches each candidate result and reassigns existing matches when +necessary. Unused candidates are preferred to retain the inexpensive aligned +equal-Set path. Draft and persisted-snapshot comparisons retain their ordered +branch. No startup predicate, assertion, exclusion or existing budget changed. + +| Experiment | Result | +| --- | --- | +| Extended direct and receiving owners | 145 passed | +| Combined equality, receiver and neighboring run | 619 passed, zero skipped | +| Original matcher restored as mutant | 134 passed, 11 intended work failures | +| Greedy matcher without reassignment | 143 passed, 2 equality assertion failures | +| Original-mutant direct replay | One work failure; seed -310527886, path `0:0` | + +Each of eight new direct reach states counts 200 in GREEN. Each of 180 finite +receiver/path/history/cut cells counts 1. The unchanged scalar startup owner +retains its 5,512 cells, each count 1. The greedy mutant fails class/plain +assignment; missing later reach follows that early semantic failure and is not +another claimed mutant kill. + +The guarded replay-witness channel records one failing `utils.41` replay. +The same requested replay passes once on repaired production. +The repaired public probe measures 30 reads at eight members, versus 219,200 +before repair. Its plain ten-member replacement measured 0.081 ms, versus +1,859.882 ms before repair on this machine. These single local elapsed samples +are diagnostic; no test asserts timing. + +Tests use Node 24.19.0 and installed Vitest 3.2.4, at most two workers, with +coverage disabled. Targeted TypeScript checking passes. No install, lockfile +change or full build was required. Raw configs, reports, replay witness, +subjects and logs remain under +`/private/tmp/effect-startup-delta/prep-pr/set-repair/`. + +The ordinary package command, from the repository root: + +```sh +pnpm --filter @tanstack/db exec vitest run --coverage.enabled=false \ + --maxWorkers=2 tests/utils-oracle.property.test.ts \ + tests/query/set-equality-receivers-oracle.test.ts tests/utils.test.ts \ + tests/proxy-revert-oracle.property.test.ts tests/effect.test.ts \ + tests/effect-disposal-oracle.test.ts \ + tests/query/virtual-row-fields-effect-oracle.test.ts +``` + +Replay uses `TANSTACK_DB_ORACLE_PROPERTY=utils.41`, +`TANSTACK_DB_ORACLE_SEED=-310527886` and `TANSTACK_DB_ORACLE_PATH=0:0`, selecting +`41:`. Full finite receiving execution remains independent of seed replay. + +## Rubric review and limits + +This table is the author's self-review. A fresh independent reviewer also +inspected the matcher, model, driver and raw evidence and found no supported +implementation defect. That review ran no tests. Its redundant-reversal +observation is accounted for above; reversal adds no independent class coverage. + +| Requirement | Evidence | +| --- | --- | +| ORC-001 | Accepted equality authority and separate engineering work bounds are stated above. | +| ORC-002 | Authored descriptors supply expected equality independently of production. | +| ORC-003 | Literate shared model and receiving drivers state law, grammar, observations and limits. | +| ORC-004 | Reconstruction, ablations, bounds, reversal redundancy and duplicate-reference exclusion are stated above. | +| ORC-005 | Direct helper, public Collection changes and both core Effect paths compare at named cuts. Scalar sources isolate projected-value work. | +| ORC-006 | Original and greedy mutants fail the intended work and equality assertions. | +| ORC-007 | Same property and 100-run budget in fixed/fresh campaigns; one-run seed/path replay executed. | +| ORC-008 | No product lifecycle state added; the used-member subset enforces one-to-one assignment. | +| ORC-009 | Descriptors abstract authored values/prototypes and homologous links; cuts use glossary terms. | +| ORC-010 | Receivers retain causal failures at each cut, continue to next use and preserve cleanup errors. | +| ORC-011 | Subset enumeration challenges augmenting paths on non-equivalence class compatibility; arbitrary topology remains outside the model. | +| ORC-012 | Baseline identity, fingerprints, experiments and limits accompany the repair. | +| ORC-013 | Equal/reordered copies distinguish changed leaves and multiplicity; class/plain words reject greedy matching. | +| ORC-014 | Claims stop at eager controlled core sources; no provider/framework handoff is inferred. | +| ORC-015 | Eight direct states count 200; all 180 receiving cells count 1 in GREEN. | +| ORC-016 | All histories and receiving paths share the descriptor judgment. | + +The work claim covers flat members at the stated sizes and one-row eager core +receivers. It does not bound arbitrary recursive graphs, nested-Set complexity, +changing getters, proxies, large Sets, real providers/frameworks, or every +comparator caller's total work. Class/cycle evidence covers two-member words +with matching link shapes, not general graph isomorphism or all cross-member +topologies. Existing broader equality laws remain with their owners. +Fixture-construction exceptions precede the cleanup wrapper; those setup +failures are outside the cleanup evidence. +The receiving ledger observes batch delivery. Rich named-callback metadata and +opaque-key calibration remain with the Effect metadata owner. Its separate +[continuity calibration repair](effect-startup-delta.md#key-interval-checker-calibration) +does not extend this Set-work observation boundary. + +Reviewed identity is parent `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` plus +these SHA256 fingerprints: + +| File | SHA256 | +| --- | --- | +| `packages/db/src/utils.ts` | `6ee964e4cd4ef93131ce5d287dc5910cbfc218dc62cf65ee2c8897eb12d1ef28` | +| `packages/db/tests/set-equality-oracle.ts` | `d2654eb35cb3d86c9aefd7969afdb68d7ae230138da93a0c43abd89efcf592b2` | +| `packages/db/tests/utils-oracle.property.test.ts` | `5349e7dd770d73058aaf7d07f83fd81a0873a5f15e8a995b89d1a65eaa68acbb` | +| `packages/db/tests/query/set-equality-receivers-oracle.test.ts` | `1532a4b14b64503fd8dc032b73c5b82f1563547b9419fd24ad9c2a809790e9db` | diff --git a/packages/db/package.json b/packages/db/package.json index 1b8a299d76..396bb427f4 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,7 +23,7 @@ "test": "vitest --run", "test:dist": "vitest run --config vitest.dist.config.ts", "test:facade-retention": "node --expose-gc --import tsx tests/facade-retention.probe.ts", - "test:oracles": "vitest --run --coverage.enabled=false tests/reverse-index-oracle.property.test.ts tests/query/compiler/in-evaluator-oracle.property.test.ts tests/cleanup-queue-oracle.property.test.ts tests/comparison-oracle.property.test.ts tests/duration-query-equality-oracle.test.ts tests/cursor-oracle.property.test.ts tests/index-update-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/change-event-history-oracle.test.ts tests/index-suggestion-oracle.test.ts tests/btree-map-oracle.test.ts tests/paced-mutations-oracle.test.ts tests/db-client-hydration-authority-oracle.test.ts tests/local-storage-order-oracle.test.ts tests/local-storage-value-fidelity-oracle.property.test.ts tests/collection-cleanup-restart-oracle.test.ts tests/collection-gc-transaction-oracle.property.test.ts tests/collection-gc-retention-oracle.property.test.ts tests/collection-gc-mutation-oracle.property.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/transaction-ownership-oracle.property.test.ts tests/optimistic-settlement-boundaries-oracle.test.ts tests/optimistic-history-publication-oracle.test.ts tests/optimistic-history-outcomes-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-index-value-oracle.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history-oracle.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-reentrancy-oracle.test.ts tests/collection-subscription-lifecycle-publication-oracle.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-cache-options-oracle.test.ts tests/mutation-confirmation-oracle.test.ts tests/mutation-handler-types-oracle.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/identity-output-shape-oracle.test.ts tests/query/optimizer-semantics-oracle.test.ts tests/query/index-path-collision-oracle.test.ts tests/query/custom-index-order-selection-oracle.test.ts tests/query/mixed-index-order-oracle.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-alias-shadowing-oracle.test.ts tests/query/includes-outer-join-route-work-oracle.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/subquery-user-value-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state-oracle.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts tests/query/virtual-row-fields-oracle.test.ts tests/query/virtual-row-fields-effect-oracle.test.ts tests/query/virtual-row-fields-types-oracle.test.ts tests/query/where-predicate-publication-oracle.property.test.ts tests/query/join-result-key-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/live-query-deferred-acquisition-oracle.test.ts tests/flat-change-tracking-oracle.property.test.ts tests/truncate-readiness-oracle.test.ts tests/proxy-detachment-contract-oracle.test.ts tests/production-error-messages-oracle.test.ts tests/query/bucket-facade-rollback-oracle.property.test.ts tests/query/aggregate-select-oracle.property.test.ts tests/query/load-subset-joint-replay-oracle.property.test.ts", + "test:oracles": "vitest --run --coverage.enabled=false tests/reverse-index-oracle.property.test.ts tests/query/compiler/in-evaluator-oracle.property.test.ts tests/cleanup-queue-oracle.property.test.ts tests/comparison-oracle.property.test.ts tests/duration-query-equality-oracle.test.ts tests/cursor-oracle.property.test.ts tests/index-update-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/change-event-history-oracle.test.ts tests/index-suggestion-oracle.test.ts tests/btree-map-oracle.test.ts tests/paced-mutations-oracle.test.ts tests/db-client-hydration-authority-oracle.test.ts tests/local-storage-order-oracle.test.ts tests/local-storage-value-fidelity-oracle.property.test.ts tests/collection-cleanup-restart-oracle.test.ts tests/collection-gc-transaction-oracle.property.test.ts tests/collection-gc-retention-oracle.property.test.ts tests/collection-gc-mutation-oracle.property.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/transaction-ownership-oracle.property.test.ts tests/optimistic-settlement-boundaries-oracle.test.ts tests/optimistic-history-publication-oracle.test.ts tests/optimistic-history-outcomes-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-index-value-oracle.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history-oracle.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-reentrancy-oracle.test.ts tests/collection-subscription-lifecycle-publication-oracle.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-cache-options-oracle.test.ts tests/mutation-confirmation-oracle.test.ts tests/mutation-handler-types-oracle.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/identity-output-shape-oracle.test.ts tests/query/optimizer-semantics-oracle.test.ts tests/query/index-path-collision-oracle.test.ts tests/query/custom-index-order-selection-oracle.test.ts tests/query/mixed-index-order-oracle.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-alias-shadowing-oracle.test.ts tests/query/includes-outer-join-route-work-oracle.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/subquery-user-value-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state-oracle.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts tests/query/virtual-row-fields-oracle.test.ts tests/query/virtual-row-fields-effect-oracle.test.ts tests/query/set-equality-receivers-oracle.test.ts tests/query/virtual-row-fields-types-oracle.test.ts tests/query/where-predicate-publication-oracle.property.test.ts tests/query/join-result-key-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/live-query-deferred-acquisition-oracle.test.ts tests/flat-change-tracking-oracle.property.test.ts tests/truncate-readiness-oracle.test.ts tests/proxy-detachment-contract-oracle.test.ts tests/production-error-messages-oracle.test.ts tests/query/bucket-facade-rollback-oracle.property.test.ts tests/query/aggregate-select-oracle.property.test.ts tests/query/load-subset-joint-replay-oracle.property.test.ts", "bench:nested-includes": "vitest bench tests/query/includes-performance.bench.ts --run", "test:indexed-db:package": "pnpm --filter @tanstack/db... build && vitest run --config vitest.indexed-db.package.config.ts", "test:collection-gc:browser": "playwright test --config playwright.collection-gc.config.ts", diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts index 5c973f7627..ab000ed38f 100644 --- a/packages/db/src/utils.ts +++ b/packages/db/src/utils.ts @@ -194,41 +194,43 @@ export function deepEqualsInternal( return aValues.every((val) => b.has(val)) } - // Object-valued Sets are unordered. Match each value once, carrying a - // branch-local cycle map so a failed candidate cannot poison the next. - const matchValues = ( - index: number, - remaining: ReadonlyArray, - branchVisited: Map, - ): boolean => { - if (index === aValues.length) return true - const value = aValues[index] - for (const [remainingIndex, candidateIndex] of remaining.entries()) { - const candidateVisited = new Map(branchVisited) - if ( - deepEqualsInternal( - value, - bValues[candidateIndex], - candidateVisited, + // Candidate comparisons share only the active ancestors. Each recursive + // comparison removes its own pairs, so an edge's result is independent of + // other assignments. Cache edges and reassign matches instead of retrying + // permutations. Plain records can match distinct keyed classes, so a + // greedy first match alone would reject some equal Sets. + const edges: Array> = aValues.map(() => []) + const matched = bValues.map(() => -1) + const matchValue = (index: number, seen: Set): boolean => { + // Prefer unused members to keep already aligned equal Sets inexpensive. + for (const occupied of [false, true]) { + for (const [candidateIndex, candidate] of bValues.entries()) { + if ( + seen.has(candidateIndex) || + (matched[candidateIndex] !== -1) !== occupied + ) + continue + const equal = (edges[index]![candidateIndex] ??= deepEqualsInternal( + aValues[index], + candidate, + new Map(visited), draft, persistedSnapshot, - ) && - matchValues( - index + 1, - remaining.filter((_, otherIndex) => otherIndex !== remainingIndex), - candidateVisited, - ) - ) { - return true + )) + if (!equal) continue + seen.add(candidateIndex) + if ( + matched[candidateIndex] === -1 || + matchValue(matched[candidateIndex]!, seen) + ) { + matched[candidateIndex] = index + return true + } } } return false } - const result = matchValues( - 0, - bValues.map((_, index) => index), - visited, - ) + const result = aValues.every((_, index) => matchValue(index, new Set())) visited.delete(a) return result } diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index ca9c1a8927..3462d04c31 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -358,7 +358,7 @@ const publicationProperties = [ // utils-oracle and comparison-oracle number their properties in file order. const utilsProperties = Array.from( - { length: 40 }, + { length: 41 }, (_, index) => `utils.${index + 1}`, ) const comparisonProperties = Array.from( diff --git a/packages/db/tests/query/set-equality-receivers-oracle.test.ts b/packages/db/tests/query/set-equality-receivers-oracle.test.ts new file mode 100644 index 0000000000..63ddc6659f --- /dev/null +++ b/packages/db/tests/query/set-equality-receivers-oracle.test.ts @@ -0,0 +1,240 @@ +/** + * Core change-event consumers must preserve unordered Set equality without + * permutation work. The primary utils equality owner supplies the descriptor + * model. These receivers check Collection changes and ordered/unordered Effect + * callbacks at startup, replacement and next use. Scalar-only Effect sources + * isolate comparison of the projected Set from source-Collection comparison. + * + * Authority: change-event equality in the glossary, EffectConfig's complete + * deltas and DeltaEvent.previousValue. The quadratic leaf-read bound is an + * engineering guard for this accepted performance repair, not a latency SLA. + * Eager controlled core sources supply the premise. No provider, framework, + * arbitrary scheduling, general graph-work or callback-count guarantee follows. + */ +import { afterAll, expect, test } from 'vitest' +import { createCollection } from '../../src/collection/index.js' +import { createEffect } from '../../src/query/effect.js' +import { declareOracle } from '../oracle-declaration.js' +import { createOracleReach } from '../oracle-reach.js' +import { + authoredSet, + expectedSetEquality, + plainSetSpecs, + setWorkSizes, +} from '../set-equality-oracle.js' +import { mockSyncCollectionOptions, withOracleCleanup } from '../utils.js' + +type Row = { id: number; members: Set<{ value: number }> } +type Event = { type: string; value: Row; previousValue?: Row } +const paths = [`collection`, `unordered-effect`, `ordered-effect`] as const +const modes = [`equal`, `reordered`, `late-mismatch`, `multiplicity`] as const +const histories = setWorkSizes.flatMap((size) => + modes.map((mode) => ({ size, mode })), +) +const cuts = [`initial`, `replacement`, `next-use`] as const +const receivingReach = createOracleReach( + `Set equality core receivers`, + paths.flatMap((path) => + histories.flatMap(({ size, mode }) => + cuts.map((cut) => `${path}/${size}/${mode}/${cut}`), + ), + ), + { replaying: false }, +) +let entered = false +afterAll(() => { + if (entered) receivingReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `Set equality receiver reach`, + Object.fromEntries(receivingReach.counts()), + ) +}) + +function valuesAt( + history: (typeof histories)[number], + tick: number, +): Array { + const { size, mode } = history + if (tick === 2) return Array.from({ length: size }, () => 3) + if (mode === `reordered`) { + const values = Array.from({ length: size }, (_, index) => index % 2) + return tick === 0 ? values : values.reverse() + } + const values = Array.from({ length: size }, () => 0) + if (mode === `late-mismatch` && tick === 0) values[size - 1] = 1 + if (mode === `multiplicity`) { + values[size - 1] = 1 + if (tick === 1) values[0] = 1 + } + return values +} + +// Callback values remain separate immutable authored objects. Normalize only +// Set iteration order; preserve all event multiplicity and previous values. +function checkSetDelta( + events: ReadonlyArray, + before: ReadonlyArray | undefined, + after: ReadonlyArray, + path: (typeof paths)[number], +): void { + const row = (values: ReadonlyArray) => ({ + id: 1, + members: [...values].sort((a, b) => a - b), + }) + const observedRow = (value: Row) => ({ + id: value.id, + members: [...value.members] + .map((member) => member.value) + .sort((a, b) => a - b), + }) + const changed = + before !== undefined && + !expectedSetEquality(plainSetSpecs(before), plainSetSpecs(after)) + const expected = + before === undefined + ? [ + { + type: path === `collection` ? `insert` : `enter`, + value: row(after), + }, + ] + : changed + ? [{ type: `update`, value: row(after), previousValue: row(before) }] + : [] + expect( + events.map((event) => ({ + type: event.type, + value: observedRow(event.value), + ...(event.previousValue + ? { + previousValue: observedRow(event.previousValue), + } + : {}), + })), + ).toStrictEqual(expected) +} +declareOracle({ + model: [expectedSetEquality], + check: [checkSetDelta], + grammar: [histories], +}) + +for (const path of paths) { + test.each(histories)( + `${path} preserves Set equality and bounded work: %j`, + async (history) => { + entered = true + receivingReach.beginHistory() + let reads = 0 + const members = (tick: number) => + authoredSet( + plainSetSpecs(valuesAt(history, tick)), + `flat`, + () => reads++, + ) + const events: Array = [] + const scalarOptions = mockSyncCollectionOptions({ + id: `set-scalar-${path}-${history.size}-${history.mode}`, + getKey: (row: { id: number; tick: number }) => row.id, + initialData: [{ id: 1, tick: 0 }], + }) + const scalar = createCollection(scalarOptions) + const setOptions = mockSyncCollectionOptions({ + id: `set-row-${path}-${history.size}-${history.mode}`, + getKey: (row: Row) => row.id, + initialData: [{ id: 1, members: members(0) }], + }) + const source = + path === `collection` ? createCollection(setOptions) : undefined + const subscription = source?.subscribeChanges( + (changes) => events.push(...changes), + { includeInitialState: true }, + ) + const effect = + path === `collection` + ? undefined + : createEffect({ + query: (q) => { + const query = q.from({ row: scalar }) + const selected = + path === `ordered-effect` + ? query.orderBy(({ row }) => row.id) + : query + return selected.fn.select(({ row }) => ({ + id: row.id, + members: members(row.tick), + })) + }, + onBatch: (batch) => { + events.push(...batch) + }, + }) + const failures: Array = [] + await withOracleCleanup(async () => { + // A host task turn delivers queued Effect callbacks, as in the existing + // metadata owner. No provider completion is inferred from this drain. + let before: Array | undefined + for (const [tick, cut] of cuts.entries()) { + if (tick > 0) events.length = 0 + reads = 0 + if (tick > 0) { + if (source) { + setOptions.utils.begin() + setOptions.utils.write({ + type: `update`, + value: { id: 1, members: members(tick) }, + }) + setOptions.utils.commit() + } else { + scalarOptions.utils.begin() + scalarOptions.utils.write({ + type: `update`, + value: { id: 1, tick }, + }) + scalarOptions.utils.commit() + } + } + await new Promise((resolve) => setTimeout(resolve, 0)) + const work = reads + const after = valuesAt(history, tick) + receivingReach.reach(`${path}/${history.size}/${history.mode}/${cut}`) + try { + checkSetDelta(events, before, after, path) + if (tick > 0) + expect(work, `quadratic receiver leaf reads`).toBeLessThanOrEqual( + 16 * (history.size + 1) ** 2, + ) + } catch (error) { + failures.push( + new Error( + `${cut}: ${error instanceof Error ? error.message : String(error)}`, + { cause: error }, + ), + ) + } + before = after + } + if (failures.length) + throw new AggregateError( + failures, + failures.map((error) => error.message).join(`\n`), + { + cause: failures[0], + }, + ) + }, [ + async () => { + await effect?.dispose() + }, + () => subscription?.unsubscribe(), + async () => { + await source?.cleanup() + }, + async () => { + await scalar.cleanup() + }, + ]) + }, + ) +} diff --git a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts index 89f670837f..c1f95bd1a6 100644 --- a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts @@ -798,8 +798,16 @@ test.each(effectFaults)( if (fault === `missing-named`) hostile.named.pop() if (fault === `delayed-extra`) hostile.batches.push([structuredClone(first)]) - if (fault === `key-collision`) second.key = first.key - if (fault === `key-change`) first.key = `different-opaque-key` + // Both channels agree on each bad key, so only interval continuity can + // reject these faults; cross-channel agreement cannot mask its removal. + if (fault === `key-collision`) { + second.key = first.key + hostile.named[1]!.key = second.key + } + if (fault === `key-change`) { + first.key = `different-opaque-key` + hostile.named[0]!.key = first.key + } expect(() => checkEffectDelta(hostile, expected, new Map(active))).toThrow() }, ) diff --git a/packages/db/tests/set-equality-oracle.ts b/packages/db/tests/set-equality-oracle.ts new file mode 100644 index 0000000000..1cb9267d5f --- /dev/null +++ b/packages/db/tests/set-equality-oracle.ts @@ -0,0 +1,186 @@ +/** + * Change-event equality matches unordered Set members once each. Distinct + * objects can have the same value, so their multiplicity still matters. + * Authority: deepEquals's supported Set domain and the glossary's class/plain + * compatibility rule. A plain record can match either keyed class; two + * different classes cannot match. Greedy matching therefore is insufficient. + * + * The model enumerates assignments of authored descriptors. It never reads + * production objects or calls the production comparator. Homologous self and + * ancestor cycles exercise the same member law; arbitrary graph isomorphism, + * changing getters, proxies and mutation during comparison are excluded. + * Pure getters count leaf reads. The accepted performance repair requires + * quadratic work for flat one-field members, not a numerical latency promise. + */ +import { expect } from 'vitest' +import { deepEquals } from '../src/utils.js' +import { declareOracle } from './oracle-declaration.js' + +export type SetMemberSpec = { + kind: `plain` | `first` | `second` + value: number +} +export type SetShape = `flat` | `self` | `owner` | `nested-owner` | `shared` +type Member = { value: number; next?: unknown } +type Equal = (left: unknown, right: unknown) => boolean + +class FirstMember { + constructor(public value: number) {} +} +class SecondMember { + constructor(public value: number) {} +} + +export function expectedSetEquality( + left: ReadonlyArray, + right: ReadonlyArray, +): boolean { + if (left.length !== right.length) return false + // At most eight members in this grammar. Memoized subset enumeration is + // an independent, small specification, not the production matching walk. + const answers = new Map() + const assign = (index: number, used: number): boolean => { + if (index === left.length) return true + const known = answers.get(used) + if (known !== undefined) return known + const member = left[index]! + const result = right.some( + (candidate, slot) => + (used & (1 << slot)) === 0 && + member.value === candidate.value && + (member.kind === `plain` || + candidate.kind === `plain` || + member.kind === candidate.kind) && + assign(index + 1, used | (1 << slot)), + ) + answers.set(used, result) + return result + } + return assign(0, 0) +} + +// The driver allocates every Set member separately, including repeated values. +// Only the declared shared-leaf shape aliases an acyclic child within one Set. +export function authoredSet( + specs: ReadonlyArray, + shape: SetShape, + read: () => void, +): Set { + const set = new Set() + const shared = { leaf: 1 } + for (const spec of specs) { + const member: Member = + spec.kind === `first` + ? new FirstMember(spec.value) + : spec.kind === `second` + ? new SecondMember(spec.value) + : { value: spec.value } + Object.defineProperty(member, `value`, { + enumerable: true, + get: () => { + read() + return spec.value + }, + }) + if (shape === `self`) member.next = member + if (shape === `owner`) member.next = set + if (shape === `nested-owner`) member.next = new Set([set]) + if (shape === `shared`) member.next = shared + set.add(member) + } + return set +} + +export function plainSetSpecs( + values: ReadonlyArray, +): Array { + return values.map((value) => ({ kind: `plain`, value })) +} + +export function checkSetEquality( + left: ReadonlyArray, + right: ReadonlyArray, + shape: SetShape, + equal: Equal = deepEquals, +): void { + let reads = 0 + const a = authoredSet(left, shape, () => reads++) + const b = authoredSet(right, shape, () => reads++) + const expected = expectedSetEquality(left, right) + for (const [from, to] of [ + [a, b], + [b, a], + ] as const) { + reads = 0 + const actual = equal(from, to) + expect(actual, `Set member assignment (${shape})`).toBe(expected) + // Flat members have one numeric field. At most one comparison per member + // pair needs two leaf reads. Cardinality rejection needs none. + if (shape === `flat`) + expect(reads, `quadratic Set member reads`).toBeLessThanOrEqual( + 2 * left.length * right.length, + ) + } +} + +export const setShapes: ReadonlyArray = [ + `flat`, + `self`, + `owner`, + `nested-owner`, + `shared`, +] +export const setWorkSizes = [1, 2, 4, 6, 8] as const +export const setMemberKinds: ReadonlyArray = [ + `plain`, + `first`, + `second`, +] + +// Two-member class words exhaust the smallest non-equivalence matching domain. +// In particular [plain, first] vs [first, second] requires reassignment. +export function checkSetFamily( + value: number, + reverse: boolean, + reach: ( + state: SetShape | `equal-duplicates` | `late-mismatch` | `cardinality`, + ) => void, +): void { + const words = setMemberKinds.flatMap((first) => + setMemberKinds.map((second) => [first, second]), + ) + for (const shape of setShapes) { + for (const left of words) { + for (const right of words) { + const specs = (kinds: typeof left) => + kinds.map((kind) => ({ kind, value })) + const a = specs(left) + const b = specs(reverse ? [...right].reverse() : right) + checkSetEquality(a, b, shape) + } + } + reach(shape) + } + // Nearby equal, changed-leaf and changed-multiplicity values all use the + // same assignment model. Late mismatch reconstructs the reported slowdown. + for (const size of setWorkSizes) { + const same = Array.from({ length: size }, () => value) + const different = [...same.slice(1), value + 1] + checkSetEquality( + plainSetSpecs(same), + plainSetSpecs([...same].reverse()), + `flat`, + ) + reach(`equal-duplicates`) + checkSetEquality(plainSetSpecs(different), plainSetSpecs(same), `flat`) + reach(`late-mismatch`) + checkSetEquality(plainSetSpecs(same.slice(1)), plainSetSpecs(same), `flat`) + reach(`cardinality`) + } +} + +declareOracle({ + model: [expectedSetEquality], + check: [checkSetEquality, checkSetFamily], + grammar: [setShapes, setWorkSizes, setMemberKinds], +}) diff --git a/packages/db/tests/utils-oracle.property.test.ts b/packages/db/tests/utils-oracle.property.test.ts index 322e197ef1..b58564f442 100644 --- a/packages/db/tests/utils-oracle.property.test.ts +++ b/packages/db/tests/utils-oracle.property.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { afterAll, describe, expect, it } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' import { deepEquals } from '../src/utils' @@ -15,6 +15,8 @@ import { expectEqualityPair, expectRingLaw, } from './deep-equals-support' +import { createOracleReach } from './oracle-reach.js' +import { checkSetFamily, setShapes } from './set-equality-oracle.js' /** * The `deepEquals` API contract in `../src/utils.ts`, reinforced by the @@ -814,4 +816,32 @@ describe(`deepEquals property-based tests`, () => { }, ) }) + // Append this property so existing numeric replay selectors stay stable. + // Generated payload and order choices cross every class-pair/cycle shape; + // duplicate multiplicity and a late mismatch expose factorial work. + const setReach = createOracleReach(`utils Set equality`, [ + ...setShapes, + `equal-duplicates`, + `late-mismatch`, + `cardinality`, + ]) + let enteredSetProperty = false + afterAll(() => { + if (enteredSetProperty) setReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `utils Set equality reach`, + Object.fromEntries(setReach.counts()), + ) + }) + describe(`ambiguous Set membership`, () => { + utilsProperty([fc.integer({ min: -100, max: 100 }), fc.boolean()])( + `matches ambiguous object Sets with quadratic flat-member reads`, + (value, reverse) => { + enteredSetProperty = true + setReach.beginHistory() + checkSetFamily(value, reverse, (state) => setReach.reach(state)) + }, + ) + }) }) From d97c1aaf412661e8d5d3e0d528fe5c4667f69d65 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Oct 2026 14:23:46 -0600 Subject: [PATCH 4/8] docs: include Set comparison work in Effect changeset --- .changeset/fix-effect-left-join-startup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-effect-left-join-startup.md b/.changeset/fix-effect-left-join-startup.md index 7ca7f3ec13..ebfdaa2537 100644 --- a/.changeset/fix-effect-left-join-startup.md +++ b/.changeset/fix-effect-left-join-startup.md @@ -2,4 +2,4 @@ '@tanstack/db': patch --- -Prevent phantom Effect update callbacks for unchanged intermediate LEFT-join rows during startup. +Prevent phantom Effect update callbacks for unchanged intermediate LEFT-join rows during startup. Avoid factorial comparison work when projected Sets contain distinct objects with equal contents and a later mismatch. From 375e9b26ca93b19cc266dd1ccdfcd10e97e820fc Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Oct 2026 15:54:39 -0600 Subject: [PATCH 5/8] fix(db): classify Effect membership and remove Set matching recursion Law: Effect deltas refine before/after result membership, independently of projection equality. Set equality requires complete one-to-one assignment. Reach: 120 added Effect cells once; Set assignment/isolated 200 each; 8192-member positive/negative scale cells once. Existing reach preserved. RED: Seven targeted suites on unchanged d97c1aaf: 631 pass, 16 fail (14 Effect membership assertions, two Set matching stack overflows). GREEN: Same seven suites: 647 pass. Another 22 diagnostic probes pass. Mutant: Original classifier fails 14; net-only fails 24; greedy matching fails four campaigns and captured utils.42 replay; old recursion overflows. Limits: Finite eager controlled core histories and descriptor families; no arbitrary provider/framework schedules or nested-graph-depth claim. Evaluate all eight high-review findings in the versioned evidence record. No full builds, lowered budgets, relaxed assertions or merge authorization. --- .changeset/fix-effect-left-join-startup.md | 4 +- docs/contributing/oracle-coverage.md | 23 ++ .../effect-startup-high-review.md | 193 ++++++++++++ packages/db/src/query/effect.ts | 40 +-- packages/db/src/utils.ts | 99 ++++-- packages/db/tests/oracle-config.ts | 2 +- .../virtual-row-fields-effect-oracle.test.ts | 281 ++++++++++++++++++ packages/db/tests/set-equality-oracle.ts | 99 ++++++ .../db/tests/utils-oracle.property.test.ts | 54 +++- 9 files changed, 745 insertions(+), 50 deletions(-) create mode 100644 docs/contributing/oracle-reviews/effect-startup-high-review.md diff --git a/.changeset/fix-effect-left-join-startup.md b/.changeset/fix-effect-left-join-startup.md index ebfdaa2537..d244cd24a9 100644 --- a/.changeset/fix-effect-left-join-startup.md +++ b/.changeset/fix-effect-left-join-startup.md @@ -2,4 +2,6 @@ '@tanstack/db': patch --- -Prevent phantom Effect update callbacks for unchanged intermediate LEFT-join rows during startup. Avoid factorial comparison work when projected Sets contain distinct objects with equal contents and a later mismatch. +Classify Effect deltas from preceding result membership so LEFT-join startup and coalesced source writes emit the correct enter, update, or exit callback, including functional projections with callable or opaque values. Suppress updates when the projected result remains equal under change-event equality, and avoid that comparison when no update or batch handler observes it. + +Avoid factorial comparison work, redundant ancestor-map copies, and recursive member matching for object Sets. Preserve one-to-one matching while taking a direct path for aligned members. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index b73481bd86..9629d4a530 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -1900,6 +1900,15 @@ reach count 1. Original-matcher and greedy mutants fail work and equality assertions respectively. This bounds the stated flat-member family, not nested graph complexity, changing getters, arbitrary graphs or every comparator caller. +The subsequent high-review repair adds `utils.42`: generated Gray-code +assignment chains vary length (4–64 members), insertion order, and an isolated +column. Both 100-run campaigns reach `assignment` and `isolated` 200 times +combined. Two finite 8,192-member witnesses use the same independent bijection +or isolated-column judgment. They expose recursive matching depth, independently +of arbitrary nested-value depth. The iterative matcher preserves reassignment, +shares active ancestors, and takes a direct diagonal path for aligned members. +See [the high-review evidence](oracle-reviews/effect-startup-high-review.md). + The [Duration query equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/duration-query-equality-oracle.test.ts) uses ten public fields to predict direct BasicIndex/BTreeIndex exact lookups and public `eq` snapshots before and after indexing. The `in` evaluator owner also @@ -5809,6 +5818,20 @@ Native histories and all 5,512 reach cells are unchanged. See the follow-up's [calibration evidence](oracle-reviews/effect-startup-delta.md#key-interval-checker-calibration) for the new owner fingerprint and the limits of the continuity claim. +The subsequent high-review repair extends the same owner with 24 finite +histories crossing scalar/callable/private-field payloads, one source/LEFT join, +initially absent/present membership, and separate/coalesced source transactions. +The independent source snapshots still determine the full delta. Both callback +channels preserve event multiplicity, previous readable payloads, and active key +intervals across initial, sequence, absent, reentry and next-use cuts. All 120 +new reach cells have count 1. On unchanged `d97c1aaf4`, 14 histories fail; +retaining preceding result membership repairs them without relying on payload +equality. The original classifier and a net-count-only classifier fail these +comparisons. This finite extension does not claim exact rich-payload instance +identity, arbitrary nondeterministic projections, changing join keys, provider +schedules or framework behavior. The earlier scalar and ordered-repair limits +remain. See [the high-review evidence](oracle-reviews/effect-startup-high-review.md). + ### RC bound schema mutation receivers (2026-10-28) Owner: `packages/db/tests/live-query-bound-options-oracle.ts`; shared driver: diff --git a/docs/contributing/oracle-reviews/effect-startup-high-review.md b/docs/contributing/oracle-reviews/effect-startup-high-review.md new file mode 100644 index 0000000000..e1b5e78423 --- /dev/null +++ b/docs/contributing/oracle-reviews/effect-startup-high-review.md @@ -0,0 +1,193 @@ +# Effect and Set high-review evaluation + +## Subject and laws + +The supplied review targeted PR #2301 at exact head +`d97c1aaf412661e8d5d3e0d528fe5c4667f69d65`. It contained eight hypotheses, +removed duplicates, and supplied no executed reproduction. This record evaluates +all eight. The append-only raw ledger and full logs are retained under +`/private/tmp/effect-startup-delta/high-review/`. + +Effect callbacks describe result membership before and after a publication: +absent/present enters, present/absent exits, and changed present/present updates. +`EffectConfig`, `DeltaEvent`, and the Effects delta table in the live-query guide +supply the authority. Projection equality cannot establish prior membership. +Change-event equality intentionally permits plain/keyed-class compatibility; +a source edit outside the projected result need not produce an update. + +Set change-event equality requires a complete one-to-one assignment, including +plain/keyed-class overlap and homologous cycles. The glossary, `deepEquals`, and +the existing descriptor owner supply the authority. Matching many flat members +must return its equality judgment without consuming one JavaScript stack frame +per member. AGENTS.md's efficiency guidance also motivates removing avoidable +allocation and scanning; these measurements establish no latency SLA. + +## Experiments before production edits + +The whole-review plan grouped membership/value/work findings 1–4, Set assignment +and work findings 5–7, and generator-integrity finding 8. Production files were +byte-identical to the reviewed head until the extended oracle completed RED. + +The Effect owner adds 24 finite histories: scalar/callable/private-field payload, +one source/LEFT join, initially absent/present, and separate/coalesced source +transactions. Public sync writes inside a real optimistic transaction's scheduler +scope reconstruct both reported three-write sequences. Independent source +snapshots determine the expected delta. Both callback channels compare event +multiplicity, previous readable values, and active opaque-key intervals at +initial, sequence, absent, reentry and next-use cuts. The rich recorder preserves +payload kind and readable result; it makes no exact instance-identity claim. +The 120 new reach cells all execute once. Original 200 histories remain intact. + +The Set owner adds Gray-code descriptor chains. A supplied complete bijection +proves the positive judgment; an isolated column proves the negative judgment. +This small partial specification does not reproduce production's matching walk. +`utils.42` generates lengths 4–64, insertion order and both judgments in each +history, with the existing 100-run fixed/fresh campaign and replay interface. +Two finite 8,192-member cases run through the same model, driver and comparison. +An ordinary source getter and observer-only transforms measured work separately. +No framework/provider behavior is inferred from controlled eager sources. + +Unchanged production: **631 passed, 16 failed**, across seven targeted suites. +Fourteen Effect histories fail membership assertions; both finite Set witnesses +throw `RangeError: Maximum call stack size exceeded` during real comparison. +The original 216 Effect tests and other five neighboring suites pass. The +instrumented classifier sees balanced absent startup contributions and the +reported 2/1 and 1/2 insert/delete multiplicities on the real public path. +There is no missing-source-information hypothesis at these cuts. + +The exact seven-suite command, used for RED and GREEN, was: + +```sh +TANSTACK_DB_ORACLE_STATISTICS=1 node node_modules/vitest/vitest.mjs run \ + --config /private/tmp/effect-startup-delta/prep-pr/set-repair/final.config.mjs \ + --reporter=default --reporter=json \ + --outputFile=/private/tmp/effect-startup-delta/high-review/green-sparse-final.json +``` + +The scratch config selects utils, utils oracle, Set receivers, proxy revert, +Effect, Effect disposal and Effect metadata suites. It uses jsdom, the package +test setup, a source alias for `@tanstack/db-ivm`, two workers and no coverage. +The RED report is `extended-red-final.json`; the same selected paths and budgets +run in GREEN. Mutant configs differ only by the named source transform. +Replay adds `TANSTACK_DB_ORACLE_PROPERTY=utils.42`, +`TANSTACK_DB_ORACLE_SEED=-75265719`, `TANSTACK_DB_ORACLE_PATH=1`, and `-t '42:'`. + +## Repair and inference + +Every Effect now retains its preceding coherent result membership, including a +skipped initial baseline. Membership plus the net multiplicity decides enter, +exit or continued presence. Value equality decides only a continued row's +update. Retaining rows also preserves the previous callback value; this costs +one map entry and retained row per current result. There is no output Collection. +When neither update nor batch is observed, the runner refreshes retained values +without comparing or dispatching an update, preserving a later exit's value. + +Set matching now uses an explicit augmenting-path stack, cached candidate +comparisons and shared active ancestors. An aligned free diagonal needs one +candidate check. The final free column can be rejected immediately when isolated. +A displaced member tries the successor of its former assignment, then wraps +through every candidate. This retains complete reassignment rather than greedy +pairing. Edge storage is linear for aligned Sets and quadratic in the general +case; arbitrary nested graph complexity remains outside the work claim. + +The same seven suites now give **647 passed, zero failed**. All 120 new Effect +reach cells have count 1; the original 5,512 cells remain at count 1. Both Set +chain states reach 200 across the two campaigns; each large scale state reaches +once. Existing `utils.41` states remain 200 each and all 180 Set receiver cells +remain once. Targeted TypeScript, ESLint, changed-oracle lint, formatting and +whitespace checks pass. No full build, installation or lockfile edit occurred. + +Hostile experiments use Vite `enforce: pre` transforms, leaving checkout sources +unchanged: + +| Wrong design | Outcome at the intended boundary | +| --- | --- | +| Exact-head Effect classifier | 14 of 24 new histories fail delta/interval assertions; 10 pass. | +| Infer prior presence from net count alone | All 24 histories fail neighboring delta/previous-value comparisons. | +| Exact-head recursive Set matcher | Small campaigns pass; both 8,192-member comparisons overflow. This is a runtime failure after setup, not a value-assertion kill. | +| Greedy Set matcher with reassignment removed | All four `utils.41`/`utils.42` campaigns reject false inequality. | +| Direct captured replay | `utils.42`, seed `-75265719`, path `1`: one assertion failure under greedy matching, one pass under repaired production. Fixed campaign stands aside. | + +An initial transform experiment ran after TypeScript processing: whole-file +overlays failed setup, and a type-annotated greedy replacement did not install. +Those runs are retained and excluded from mutant evidence. The first iterative +breadth-first candidate was interrupted for excessive work on the finite scale +witness; no GREEN is attributed to it. The final depth-first candidate passes +without changing any reach, assertions or budgets. + +Work probes on identical inputs: + +| Input | Reviewed head | Repaired | +| --- | --- | --- | +| Effect replacement, enter/exit handlers only | 4 selected leaf reads | 2 reads; no update callback | +| Same replacement, update or batch handler | 4 reads | 4 reads; corresponding update delivered | +| Aligned object Sets, sizes 8/32/128/512 | 8/32/128/512 ancestor-map copies; 36/528/8,256/131,328 occupied-prefix checks | Zero map copies; 8/32/128/512 candidate calls | + +## Finding ledger and reviewer assessment + +Technical validity, PR action and durable value remain separate. Raw findings +retain their source-order IDs in the scratch ledger. + +| ID | Technical verdict and severity | PR action / durable value | +| --- | --- | --- | +| HIGH2301-1 | Confirmed, high: callable and opaque payloads expose the phantom startup update. Nondeterminism is unnecessary to establish the defect. | Fixed now; retain representation-independent membership histories. No general promise about nondeterministic projections is introduced. | +| HIGH2301-2 | Confirmed, high: both grouped three-write sequences emit update instead of enter/exit. Net count alone is insufficient. | Fixed now; retained membership plus net count, with separate/coalesced and next-use witnesses. | +| HIGH2301-3 | Described suppression is real; the correctness accusation conflicts with accepted projected-result/change-event equality semantics. | Accepted design for semantics. Changeset now explicitly documents general equal-projection suppression and class/plain compatibility remains tested. | +| HIGH2301-4 | Confirmed avoidable work, low; no measured latency consequence. | Fixed now; handler demand bypasses comparison while retained state stays current. Preserve the deterministic work probe. | +| HIGH2301-5 | Confirmed redundant per-pair map allocation, low. | Fixed now; share active ancestors, preserving cycle and assignment comparisons. | +| HIGH2301-6 | Confirmed quadratic occupied-prefix scanning for aligned Sets, medium. | Fixed now; direct diagonal path measured at one candidate call per member. No general linear-matching claim. | +| HIGH2301-7 | Confirmed flat-member stack overflow, medium. The reviewed code inherited this risk. | Fixed now; iterative reassignment with shared generated and finite scale judgments. Arbitrary nested depth is distinct. | +| HIGH2301-8 | Refuted as stated: ORC permits bounded exhaustive structural dimensions. The scalar arbitrary generates real variation. | No grammar removal. Same-arbitrary sample at fixed seed has 69 scalar values and both reversals; reach 200 never meant 200 distinct shapes. New structural-length coverage belongs to finding 7. | + +Loss audit: **8 = 6 fixed-now + 1 accepted-design + 1 refuted**. There are no +review-item deferrals, duplicate dispositions or known open violations in this +bounded repair. The documentation subclaim of item 3 is also repaired. There +are no newly confirmed sibling defects outside those IDs. This is a self-review; +no new independent reviewer was delegated. + +Reviewer recommendation: **lean hire for defect discovery, with verification +required before approval**. Prioritization was good: both leading hypotheses +reproduced through public APIs. Six useful engineering findings survived. The +report supplied no executed evidence, suggested net count without prior +membership, treated accepted equality as a correctness defect, and incorrectly +called finite enumeration pinned-only coverage. It is useful hypothesis input, +not sufficient PR approval evidence. + +## Oracle conformance and limits + +| Requirement | Evidence / applicability | +| --- | --- | +| ORC-001–003 | Accepted authority, explicit limits, independent snapshot/descriptor judgments and prose beside contract, grammar, driver and check. | +| ORC-004 | Reconstruction: both coalesced sequences and long chains. Ablation: representations expose equality-dependent startup; grouping exposes net membership; length exposes flat recursion; isolated column distinguishes false. Bounds are 24 finite histories, generated 4–64 and finite 8,192 members. Illegal source actions and unsupported non-witness descriptor graphs are excluded. | +| ORC-005–006 | Actual public callbacks/direct equality run before edits; both channels and future cuts are compared. Original and adjacent wrong implementations fail as classified above. | +| ORC-007 | `utils.42` runs identical fixed/fresh 100-run campaigns. Captured seed/path replay executes directly. Finite histories and scale cases are deliberately exhaustive/fixed, not random campaigns. | +| ORC-008–009 | Effect expected results remain stateless source recomputation; the callback ledger retains active opaque-key intervals only. Readable rich payload is an explicit abstraction of production values. Set descriptors represent independently authored member compatibility. | +| ORC-010 | Effect failures accumulate without stopping later cuts; shared cleanup preserves primary and secondary failures. Set witnesses own no external resources. Replay retains the same false-equality violation. | +| ORC-011 | No unresolved shared semantic-fault hypothesis: source recomputation differs from weighted deltas; supplied bijection/isolation differs from matching search. These formulations are directly exercised. | +| ORC-012–014 | This versioned record identifies subject, paths, bounds and mutants. Triggering and adjacent controls include balanced/unbalanced, present/absent, scalar/rich, assigned/isolated and separate/coalesced cases. No controlled premise is credited to a real external provider or framework. | +| ORC-015–016 | Reach counts above; fixed witnesses use shared model/driver/check, with no private expected answers or ordinary unit tests added to oracle files. | + +The law is encoded and asserted at every cut within the stated finite grammar. +Evidence does not prove arbitrary histories, changing join keys/multiplicity, +provider timing, framework callbacks, exact rich-payload instance identity, or +arbitrary cycle topology/nested-value depth. Existing ordered repair, scalar +metadata, Set receiving, draft comparison and disposal tests remain green. No +assertion, equality law, run budget or reach minimum was weakened. + +Whole-PR readiness is separate: the old 26 green checks cover `d97c1aaf4`. +Final-head remote CI and review must cover the new patch before merge readiness +can be claimed. No merge is authorized by this evaluation. + +## Reviewed source fingerprints + +These SHA256 values identify the repaired executable subject against parent +`d97c1aaf412661e8d5d3e0d528fe5c4667f69d65`. They exclude this evidence record. + +```text +6835de1fc044efec14f80ce7aa546451c9c1ce6134a1c5198779ee5ebd45c8b9 packages/db/src/query/effect.ts +1cad064d83d744c0616fe84066b19b753fbad00d543b69f6944bfb06de2f0ff7 packages/db/src/utils.ts +469415454de90a75db84caaa5b6893ae8fb54a1f85536872070f9a3c54ded1a6 packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts +6203a3d9bd5663a5ee7c14b5c66e0e139bbf7fe2d9afb09650cd01e31c1760f1 packages/db/tests/set-equality-oracle.ts +c84a3d24f2ed2cbb251c5ea758c36a36234fef18e09d82a608929d696e1d6816 packages/db/tests/utils-oracle.property.test.ts +043e546f9a2bf3ba52395c8c9d203456bf63f4fa481b0b4883fe4a56b24f448e packages/db/tests/oracle-config.ts +``` diff --git a/packages/db/src/query/effect.ts b/packages/db/src/query/effect.ts index bd777b10d8..bae626604a 100644 --- a/packages/db/src/query/effect.ts +++ b/packages/db/src/query/effect.ts @@ -337,6 +337,7 @@ export function createEffect< effect, query: config.query, skipInitial: config.skipInitial ?? false, + observesUpdates: Boolean(config.onUpdate || config.onBatch), onBatchProcessed, onHealthChange: config.onHealthChange, onSourceError: (error: Error) => { @@ -401,6 +402,7 @@ interface EffectPipelineRunnerConfig< > { query: EffectQueryInput skipInitial: boolean + observesUpdates: boolean onBatchProcessed: (events: Array>) => void /** Called when a source collection enters error or cleaned-up state */ onSourceError: (error: Error) => void @@ -483,12 +485,13 @@ class EffectPipelineRunner { // Output accumulator private pendingChanges: Map> = new Map() - // Callback-visible result. Held repair deltas are classified against this - // last coherent membership rather than their net multiplicity alone. + // The preceding coherent result, including the skipped initial baseline. + // Multiplicity and value equality alone cannot determine prior membership. private readonly publishedRows = new Map() // skipInitial state private readonly skipInitial: boolean + private readonly observesUpdates: boolean private initialLoadComplete = false // The Effect's initial gate: latched when every // initial load has settled, with or without skipInitial. After it, @@ -514,6 +517,7 @@ class EffectPipelineRunner { constructor(config: EffectPipelineRunnerConfig) { this.skipInitial = config.skipInitial + this.observesUpdates = config.observesUpdates this.onBatchProcessed = config.onBatchProcessed this.onSourceError = config.onSourceError this.onHealthChange = config.onHealthChange @@ -1059,7 +1063,6 @@ class EffectPipelineRunner { } const shouldPublish = !this.skipInitial || this.initialLoadComplete - const tracksPublishedRows = this.orderedLoaders.size > 0 const events: Array> = [] @@ -1067,14 +1070,14 @@ class EffectPipelineRunner { const event = classifyDelta( key as TKey, changes, - tracksPublishedRows ? this.publishedRows : undefined, + this.publishedRows, + shouldPublish && this.observesUpdates, ) if (event) { - if (tracksPublishedRows) { - if (event.type === `exit`) this.publishedRows.delete(key) - else this.publishedRows.set(key, event.value) - } - if (shouldPublish) events.push(event) + if (event.type === `exit`) this.publishedRows.delete(key) + else this.publishedRows.set(key, event.value) + if (shouldPublish && (event.type !== `update` || this.observesUpdates)) + events.push(event) } } @@ -1319,18 +1322,17 @@ function accumulateEffectChanges( return acc } -/** Interpret a D2 batch against callback-visible state when a window holds it. */ +/** Interpret a D2 batch against the preceding coherent result membership. */ function classifyDelta( key: TKey, changes: EffectChanges, - publishedRows?: ReadonlyMap, + publishedRows: ReadonlyMap, + observesUpdates: boolean, ): DeltaEvent | undefined { const { inserts, deletes, insertValue, deleteValue } = changes - const wasPresent = publishedRows ? publishedRows.has(key) : deletes > 0 - const previousValue = publishedRows?.get(key) ?? deleteValue - const isPresent = publishedRows - ? Number(wasPresent) + inserts - deletes > 0 - : inserts > 0 + const wasPresent = publishedRows.has(key) + const previousValue = publishedRows.get(key) ?? deleteValue + const isPresent = Number(wasPresent) + inserts - deletes > 0 if (!wasPresent && isPresent) { // Row entered the query result @@ -1347,10 +1349,10 @@ function classifyDelta( wasPresent && isPresent && insertValue !== undefined && - !deepEquals(previousValue, insertValue) + (!observesUpdates || !deepEquals(previousValue, insertValue)) ) { - // Equal insert/delete values can cancel an intermediate LEFT-join row - // within this graph run. Only a changed final value is an update. + // Presence is already established. Compare values only for a consumer that + // observes updates; otherwise refresh the retained value for a later exit. return { type: `update`, key, diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts index ab000ed38f..7a5cc3983f 100644 --- a/packages/db/src/utils.ts +++ b/packages/db/src/utils.ts @@ -199,38 +199,81 @@ export function deepEqualsInternal( // other assignments. Cache edges and reassign matches instead of retrying // permutations. Plain records can match distinct keyed classes, so a // greedy first match alone would reject some equal Sets. - const edges: Array> = aValues.map(() => []) - const matched = bValues.map(() => -1) - const matchValue = (index: number, seen: Set): boolean => { - // Prefer unused members to keep already aligned equal Sets inexpensive. - for (const occupied of [false, true]) { - for (const [candidateIndex, candidate] of bValues.entries()) { - if ( - seen.has(candidateIndex) || - (matched[candidateIndex] !== -1) !== occupied - ) - continue - const equal = (edges[index]![candidateIndex] ??= deepEqualsInternal( - aValues[index], - candidate, - new Map(visited), - draft, - persistedSnapshot, - )) - if (!equal) continue - seen.add(candidateIndex) - if ( - matched[candidateIndex] === -1 || - matchValue(matched[candidateIndex]!, seen) - ) { - matched[candidateIndex] = index - return true - } + // Cache diagonal comparisons separately: aligned Sets need linear storage + // and no candidate scan. Other rows cache only the pairs actually tried. + // Zero is unknown, one is unequal, and two is equal. + const diagonal = new Uint8Array(aValues.length) + const edges: Array | undefined> = [] + const equalAt = (left: number, right: number): boolean => { + if (left === right && diagonal[left] !== 0) return diagonal[left] === 2 + const row = + left === right + ? undefined + : (edges[left] ??= new Map()) + const cached = row?.get(right) + if (cached !== undefined) return cached + const equal = deepEqualsInternal( + aValues[left], + bValues[right], + visited, + draft, + persistedSnapshot, + ) + if (left === right) diagonal[left] = equal ? 2 : 1 + else row!.set(right, equal) + return equal + } + const matchedRight = bValues.map(() => -1) + const matchedLeft = aValues.map(() => -1) + const matchValue = (index: number): boolean => { + if (matchedRight[index] === -1 && equalAt(index, index)) { + matchedRight[index] = index + matchedLeft[index] = index + return true + } + // An isolated final column cannot participate in any complete assignment. + // Check it directly rather than rediscovering that through every path. + if (index === aValues.length - 1) { + const free = matchedRight.indexOf(-1) + if (aValues.every((_, left) => !equalAt(left, free))) return false + } + // Find an augmenting path without using JavaScript call depth for the + // number of members. Each right predecessor names the left that found it. + const predecessors = new Int32Array(bValues.length).fill(-1) + const stack = [{ left: index, next: 0 }] + while (stack.length > 0) { + const frame = stack[stack.length - 1]! + if (frame.next === bValues.length) { + stack.pop() + continue + } + // After displacement, try the successor of the former assignment + // first. Wrap once so every candidate remains eligible. + const start = + matchedLeft[frame.left] === -1 + ? frame.left + : matchedLeft[frame.left]! + 1 + const right = (start + frame.next++) % bValues.length + if (predecessors[right] !== -1 || !equalAt(frame.left, right)) continue + predecessors[right] = frame.left + if (matchedRight[right] !== -1) { + stack.push({ left: matchedRight[right]!, next: 0 }) + continue + } + // Flip every edge back to the root, releasing each old assignment. + let slot = right + while (slot !== -1) { + const owner = predecessors[slot]! + const previous = matchedLeft[owner]! + matchedRight[slot] = owner + matchedLeft[owner] = slot + slot = previous } + return true } return false } - const result = aValues.every((_, index) => matchValue(index, new Set())) + const result = aValues.every((_, index) => matchValue(index)) visited.delete(a) return result } diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 3462d04c31..f61d2d1dba 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -358,7 +358,7 @@ const publicationProperties = [ // utils-oracle and comparison-oracle number their properties in file order. const utilsProperties = Array.from( - { length: 41 }, + { length: 42 }, (_, index) => `utils.${index + 1}`, ) const comparisonProperties = Array.from( diff --git a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts index c1f95bd1a6..30db585a49 100644 --- a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts @@ -48,6 +48,7 @@ import { afterAll, expect, test } from 'vitest' import { createCollection } from '../../src/collection/index.js' import { Query, coalesce, eq } from '../../src/query/index.js' import { createEffect } from '../../src/query/effect.js' +import { createTransaction } from '../../src/transactions.js' import { declareOracle } from '../oracle-declaration.js' import { createOracleReach } from '../oracle-reach.js' import { flushPromises, mockSyncCollectionOptions } from '../utils.js' @@ -927,3 +928,283 @@ test(`preserves later interval judgments after an invalid unknown update`, () => const update = expectedEffectDelta(before, after) checkEffectDelta(calibrationLedger(update, keyFor), update, active) }) + +/** + * Membership is independent of the selected value's comparison domain. A fresh + * callable or opaque instance may compare unequal when the graph retracts a + * transient join contribution, but an absent occurrence still cannot update. + * These histories also coalesce legal source transactions inside a real + * optimistic-transaction scheduler scope. Their before/after source snapshots + * determine membership independently of output multiplicities. + * + * The rich-value observation records its kind and callable/readable result. + * It does not claim exact instance identity for a re-evaluated projection. + * Nondeterministic projections are not needed to expose this membership law. + */ +type BoundaryRepresentation = `scalar` | `function` | `private` +class BoundaryValue { + #label: string + constructor(label: string) { + this.#label = label + } + read() { + return this.#label + } +} +const boundaryHistories = ([`scalar`, `function`, `private`] as const).flatMap( + (representation) => + ([`single`, `join`] as const).flatMap((kind) => + [false, true].flatMap((present) => + [false, true].map((grouped) => ({ + representation, + kind, + present, + grouped, + })), + ), + ), +) +function boundaryId(history: (typeof boundaryHistories)[number]) { + return `${history.representation}/${history.kind}/${history.present}/${history.grouped}` +} +const boundaryCuts = [`initial`, `sequence`, `absent`, `reentry`, `next-use`] +const boundaryReach = createOracleReach( + `Effect membership independent of projection equality`, + boundaryHistories.flatMap((history) => + boundaryCuts.map((cut) => `${boundaryId(history)}/${cut}`), + ), + { replaying: false }, +) +let enteredBoundary = false +afterAll(() => { + if (enteredBoundary) boundaryReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `Effect membership reach`, + Object.fromEntries(boundaryReach.counts()), + ) +}) +function boundaryModelRows( + kind: `single` | `join`, + representation: BoundaryRepresentation, + left: SourceState, + right: SourceState, +): Array { + return expectedEffectRows(kind, left, right).map((row) => ({ + ...row, + payloadKind: representation, + payload: row.label, + })) +} +function captureBoundaryEvent(event: DeltaEvent): RecordedEvent { + const capture = (value: unknown) => { + const row = captureSelected(value) as Record + const payload = row.payload + return { + ...row, + payloadKind: + typeof payload === `function` + ? `function` + : payload instanceof BoundaryValue + ? `private` + : `scalar`, + payload: + typeof payload === `function` + ? payload() + : payload instanceof BoundaryValue + ? payload.read() + : payload, + } + } + return { + type: event.type, + key: event.key, + value: capture(event.value), + ...(`previousValue` in event + ? { previousValue: capture(event.previousValue) } + : {}), + } +} +declareOracle({ model: [boundaryModelRows], grammar: [boundaryHistories] }) +test.each(boundaryHistories)( + `classifies callback-visible membership across coalesced source histories: %j`, + async (history) => { + enteredBoundary = true + boundaryReach.beginHistory() + const main: SourceRow = { + id: `a`, + parentId: `unused`, + side: `left`, + label: `v0`, + } + const anchor: SourceRow = { + id: `anchor`, + parentId: `unused`, + side: `left`, + label: `anchor`, + } + const joined: SourceRow = { + id: `r`, + parentId: `a`, + side: `right`, + label: `right`, + } + const state: SourceState = { + id: `boundary-left`, + base: history.present ? [main, anchor] : [anchor], + origins: new Map(), + } + const rightState: SourceState = { + id: `boundary-right`, + base: [joined], + origins: new Map(), + } + const left = createSource(state, gate()) + const right = createSource(rightState, gate()) + const ledger: Ledger = { batches: [], named: [] } + const active: KeyIntervals = new Map() + const failures: Array = [] + const callbackErrors: Array = [] + let effect: ReturnType | undefined + let before: Array = [] + let checkedBatch = 0 + let checkedNamed = 0 + const cut = async (name?: string) => { + await flushPromises() + const after = boundaryModelRows( + history.kind, + history.representation, + state, + rightState, + ) + try { + checkEffectDelta( + { + batches: ledger.batches.slice(checkedBatch), + named: ledger.named.slice(checkedNamed), + }, + expectedEffectDelta(before, after), + active, + ) + expect(callbackErrors).toStrictEqual([]) + } catch (error) { + failures.push( + new Error(`membership cut ${name ?? `individual action`}`, { + cause: error, + }), + ) + } + before = after + checkedBatch = ledger.batches.length + checkedNamed = ledger.named.length + if (name) boundaryReach.reach(`${boundaryId(history)}/${name}`) + } + // The model applies the authored source operation directly. Production + // receives the same operation through public sync writes and scheduling. + const apply = (type: `insert` | `delete` | `update`, label: string) => { + const row = { ...main, label } + state.base = + type === `delete` + ? state.base.filter((value) => value.id !== main.id) + : type === `insert` + ? [...state.base, row] + : state.base.map((value) => (value.id === main.id ? row : value)) + left.utils.begin() + left.utils.write({ type, value: row }) + left.utils.commit() + } + await checkWithCleanup(async () => { + await Promise.all([left.preload(), right.preload()]) + effect = createEffect({ + query: () => { + const base = new Query().from({ a: left }) + const project = (a: SourceValue, b?: SourceValue) => { + const label = a.label + const payload = + history.representation === `function` + ? () => label + : history.representation === `private` + ? new BoundaryValue(label) + : label + return { + ...selectSource(a), + ...(history.kind === `join` + ? { + rightLabel: b?.label, + rightKey: b?.$key, + rightCollectionId: b?.$collectionId, + rightOrigin: b?.$origin, + rightPending: b?.$hasPendingWrites, + } + : {}), + payload, + } + } + return history.kind === `join` + ? base + .leftJoin({ b: right }, ({ a, b }) => eq(a.id, b.parentId)) + .fn.select(({ a, b }) => project(a, b)) + : base.fn.select(({ a }) => project(a)) + }, + onBatch: (events) => { + ledger.batches.push(events.map(captureBoundaryEvent)) + }, + onEnter: (event) => { + ledger.named.push({ route: `enter`, ...captureBoundaryEvent(event) }) + }, + onUpdate: (event) => { + ledger.named.push({ route: `update`, ...captureBoundaryEvent(event) }) + }, + onExit: (event) => { + ledger.named.push({ route: `exit`, ...captureBoundaryEvent(event) }) + }, + onError: (error) => callbackErrors.push(error), + onSourceError: (error) => callbackErrors.push(error), + }) + await cut(`initial`) + const commands: Array = + history.present + ? [ + [`delete`, `v0`], + [`insert`, `v1`], + [`delete`, `v1`], + ] + : [ + [`insert`, `v0`], + [`delete`, `v0`], + [`insert`, `v1`], + ] + if (history.grouped) { + createTransaction({ mutationFn: async () => {} }).mutate(() => { + for (const [type, label] of commands) apply(type, label) + }) + } else { + for (const [type, label] of commands) { + apply(type, label) + await cut() + } + } + await cut(`sequence`) + if (!history.present) apply(`delete`, `v1`) + await cut(`absent`) + apply(`insert`, `v2`) + await cut(`reentry`) + apply(`update`, `v3`) + await cut(`next-use`) + if (failures.length) + throw new AggregateError(failures, `Effect membership history failed`, { + cause: failures[0], + }) + }, [ + async () => { + await effect?.dispose() + }, + async () => { + await right.cleanup() + }, + async () => { + await left.cleanup() + }, + ]) + }, +) diff --git a/packages/db/tests/set-equality-oracle.ts b/packages/db/tests/set-equality-oracle.ts index 1cb9267d5f..2a0dd62dfe 100644 --- a/packages/db/tests/set-equality-oracle.ts +++ b/packages/db/tests/set-equality-oracle.ts @@ -184,3 +184,102 @@ declareOracle({ check: [checkSetEquality, checkSetFamily], grammar: [setShapes, setWorkSizes, setMemberKinds], }) + +/** + * An augmenting chain has a known bijection without solving production's + * matching problem. Gray-code columns differ in one coordinate. Each left + * word permits the two adjacent columns by using a plain record in that + * coordinate. Its other coordinates use the column's keyed classes. The final + * left word names column zero exactly. Matching every earlier word to its + * successor and the final word to zero is a complete assignment witness. + * + * A changed leaf in the final right column instead isolates that column: + * every left leaf is zero, so no left word can match it. These are sufficient + * positive and negative judgments for this grammar, not a general graph solver. + * Rotation varies Set insertion order. Flat matching depth must not become + * JavaScript call depth, even though arbitrary nested-value depth is unbounded + * by this instrument. Small generated chains and a finite 8,192-member chain + * share the same descriptor judgment and production comparison. + */ +type WordDescriptor = ReadonlyArray +export function setChainDescriptors( + bits: number, + broken: boolean, + rotate: boolean, +) { + const size = 2 ** bits + const column = (index: number): Array => { + const gray = index ^ (index >> 1) + return Array.from({ length: bits }, (_, coordinate) => ({ + kind: ((gray >> coordinate) & 1) === 0 ? `first` : `second`, + value: broken && index === size - 1 ? 1 : 0, + })) + } + const right = Array.from({ length: size }, (_, index) => column(index)) + const left = Array.from({ length: size }, (_, index) => { + const start = index === size - 1 ? 0 : index + const a = column(start).map((member) => ({ ...member, value: 0 })) + const b = column(index === size - 1 ? 0 : index + 1) + return a.map((member, coordinate): SetMemberSpec => ({ + ...member, + kind: member.kind === b[coordinate]!.kind ? member.kind : `plain`, + })) + }) + const witness = left.map((_, index) => (index + 1) % size) + if (rotate) { + right.push(right.shift()!) + for (const [index, slot] of witness.entries()) + witness[index] = (slot + size - 1) % size + } + return { left, right, witness } +} +function expectedChainEquality( + left: ReadonlyArray, + right: ReadonlyArray, + witness: ReadonlyArray, +): boolean { + const compatible = (a: WordDescriptor, b: WordDescriptor) => + a.length === b.length && + a.every( + (member, index) => + member.value === b[index]!.value && + (member.kind === `plain` || + b[index]!.kind === `plain` || + member.kind === b[index]!.kind), + ) + if ( + right.some((word) => + left.every((candidate) => !compatible(candidate, word)), + ) + ) + return false + if ( + left.length === right.length && + new Set(witness).size === right.length && + witness.every( + (slot, index) => + slot >= 0 && + slot < right.length && + compatible(left[index]!, right[slot]!), + ) + ) + return true + throw new Error( + `Chain grammar supplied neither a bijection nor an isolated-column witness`, + ) +} +export function checkSetChain(bits: number, broken: boolean, rotate: boolean) { + const { left, right, witness } = setChainDescriptors(bits, broken, rotate) + const expected = expectedChainEquality(left, right, witness) + const word = (specs: WordDescriptor) => [ + ...authoredSet(specs, `flat`, () => {}), + ] + expect(deepEquals(new Set(left.map(word)), new Set(right.map(word)))).toBe( + expected, + ) +} +declareOracle({ + model: [expectedChainEquality], + check: [checkSetChain], + grammar: [setChainDescriptors], +}) diff --git a/packages/db/tests/utils-oracle.property.test.ts b/packages/db/tests/utils-oracle.property.test.ts index b58564f442..a2d478ca29 100644 --- a/packages/db/tests/utils-oracle.property.test.ts +++ b/packages/db/tests/utils-oracle.property.test.ts @@ -16,7 +16,11 @@ import { expectRingLaw, } from './deep-equals-support' import { createOracleReach } from './oracle-reach.js' -import { checkSetFamily, setShapes } from './set-equality-oracle.js' +import { + checkSetChain, + checkSetFamily, + setShapes, +} from './set-equality-oracle.js' /** * The `deepEquals` API contract in `../src/utils.ts`, reinforced by the @@ -845,3 +849,51 @@ describe(`deepEquals property-based tests`, () => { ) }) }) + +// The chain family varies structural length, insertion order and a neighboring +// isolated-column violation. Both campaigns use the same descriptor judgment. +// The large finite witness checks flat matching depth without multiplying that +// expensive witness by the random campaign budget. +describe(`Set assignment paths`, () => { + const chainReach = createOracleReach(`Set augmenting chains`, [ + `assignment`, + `isolated`, + ]) + let entered = false + afterAll(() => { + if (entered) chainReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info(`Set chain reach`, Object.fromEntries(chainReach.counts())) + }) + utilsProperty([fc.integer({ min: 2, max: 6 }), fc.boolean()])( + `preserves Set assignment across augmenting paths and isolated columns`, + (bits, rotate) => { + entered = true + chainReach.beginHistory() + for (const broken of [false, true]) { + checkSetChain(bits, broken, rotate) + chainReach.reach(broken ? `isolated` : `assignment`) + } + }, + ) + const scaleReach = createOracleReach( + `flat Set matching scale`, + [`positive`, `negative`], + { replaying: false }, + ) + let enteredScale = false + afterAll(() => { + if (enteredScale) scaleReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info(`Set scale reach`, Object.fromEntries(scaleReach.counts())) + }) + it.each([false, true])( + `keeps flat Set matching off the call stack: isolated=%s`, + (broken) => { + enteredScale = true + scaleReach.beginHistory() + checkSetChain(13, broken, false) + scaleReach.reach(broken ? `negative` : `positive`) + }, + ) +}) From 0b47ee825aaa0c36b199a3c42652cab8d9672ec9 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Oct 2026 15:55:07 -0600 Subject: [PATCH 6/8] docs: bind high-review evidence to verified repair commit --- .../oracle-reviews/effect-startup-high-review.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/contributing/oracle-reviews/effect-startup-high-review.md b/docs/contributing/oracle-reviews/effect-startup-high-review.md index e1b5e78423..81cd4966bc 100644 --- a/docs/contributing/oracle-reviews/effect-startup-high-review.md +++ b/docs/contributing/oracle-reviews/effect-startup-high-review.md @@ -191,3 +191,11 @@ These SHA256 values identify the repaired executable subject against parent c84a3d24f2ed2cbb251c5ea758c36a36234fef18e09d82a608929d696e1d6816 packages/db/tests/utils-oracle.property.test.ts 043e546f9a2bf3ba52395c8c9d203456bf63f4fa481b0b4883fe4a56b24f448e packages/db/tests/oracle-config.ts ``` + +## Exact reviewed repair commit + +The executable subject is `375e9b26ca93b19cc266dd1ccdfcd10e97e820fc`. +The source fingerprints above match that commit after the pre-commit hooks. +This appended entry binds the 647-test GREEN, 22 diagnostic passes, targeted +type/lint checks and eight-item audit to that exact subject. The following +documentation commit adds only this binding, without changing executable code. From 2b3dd8c2d0a0f98a858dc1d08182f6673c9a7836 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 30 Oct 2026 07:47:10 -0600 Subject: [PATCH 7/8] refactor(db): remove Set performance work from Effect fix --- .changeset/fix-effect-left-join-startup.md | 2 - docs/contributing/oracle-coverage.md | 33 +- .../oracle-reviews/effect-startup-delta.md | 117 ++++++- .../effect-startup-high-review.md | 201 ------------ .../oracle-reviews/set-equality-work.md | 166 ---------- packages/db/package.json | 2 +- packages/db/src/utils.ts | 107 ++----- packages/db/tests/oracle-config.ts | 2 +- .../set-equality-receivers-oracle.test.ts | 240 --------------- packages/db/tests/set-equality-oracle.ts | 285 ------------------ .../db/tests/utils-oracle.property.test.ts | 84 +----- 11 files changed, 161 insertions(+), 1078 deletions(-) delete mode 100644 docs/contributing/oracle-reviews/effect-startup-high-review.md delete mode 100644 docs/contributing/oracle-reviews/set-equality-work.md delete mode 100644 packages/db/tests/query/set-equality-receivers-oracle.test.ts delete mode 100644 packages/db/tests/set-equality-oracle.ts diff --git a/.changeset/fix-effect-left-join-startup.md b/.changeset/fix-effect-left-join-startup.md index d244cd24a9..60aa17d2ac 100644 --- a/.changeset/fix-effect-left-join-startup.md +++ b/.changeset/fix-effect-left-join-startup.md @@ -3,5 +3,3 @@ --- Classify Effect deltas from preceding result membership so LEFT-join startup and coalesced source writes emit the correct enter, update, or exit callback, including functional projections with callable or opaque values. Suppress updates when the projected result remains equal under change-event equality, and avoid that comparison when no update or batch handler observes it. - -Avoid factorial comparison work, redundant ancestor-map copies, and recursive member matching for object Sets. Preserve one-to-one matching while taking a direct path for aligned members. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 9629d4a530..c19a9d37d6 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -1889,25 +1889,18 @@ The [round 7 ledger](oracle-reviews/utils-unit-retirement-round-7.md) retains the retired examples. Caller-modified built-ins and Collection publication are outside this direct-return owner. -The [Set-work extension](oracle-reviews/set-equality-work.md) adds `utils.41` -and a shared descriptor judgment in `set-equality-oracle.ts`. Class/plain -assignment and homologous cycles preserve equality while flat duplicate-valued -members require quadratic leaf reads. Both 100-run campaigns reach all eight -states 200 times together. `query/set-equality-receivers-oracle.test.ts` reuses -the judgment for Collection changes and ordered/unordered Effects, including -scalar sources that synthesize projected Sets. All 180 finite receiving cells -reach count 1. Original-matcher and greedy mutants fail work and equality -assertions respectively. This bounds the stated flat-member family, not nested -graph complexity, changing getters, arbitrary graphs or every comparator caller. - -The subsequent high-review repair adds `utils.42`: generated Gray-code -assignment chains vary length (4–64 members), insertion order, and an isolated -column. Both 100-run campaigns reach `assignment` and `isolated` 200 times -combined. Two finite 8,192-member witnesses use the same independent bijection -or isolated-column judgment. They expose recursive matching depth, independently -of arbitrary nested-value depth. The iterative matcher preserves reassignment, -shares active ancestors, and takes a direct diagonal path for aligned members. -See [the high-review evidence](oracle-reviews/effect-startup-high-review.md). +Set performance work evaluated in PR #2301 is excluded by the maintainer's +2026-10-30 decision: "we've never had this perf problem ... let's remove it." +The production matcher and the existing `utils.1`–`utils.40` campaigns and +budgets remain unchanged. The synthetic duplicate-valued object-Set family +exposes factorial assignment work, and an 8,192-member flat assignment chain +exposes recursive stack depth. No application incident was established. +The [historical experiments](https://github.com/TanStack/db/blob/0b47ee825aaa0c36b199a3c42652cab8d9672ec9/docs/contributing/oracle-reviews/effect-startup-high-review.md) +retain the descriptors, observations and limits. The primary owner remains +`packages/db/tests/utils-oracle.property.test.ts`; resume optimization when the +maintainer requests it or an application workload requires it. The new +`utils.41`/`utils.42` work guards and dedicated receivers leave with that work. +This records a scope decision, not closure of those performance limitations. The [Duration query equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/duration-query-equality-oracle.test.ts) uses ten public fields to predict direct BasicIndex/BTreeIndex exact lookups and @@ -5830,7 +5823,7 @@ equality. The original classifier and a net-count-only classifier fail these comparisons. This finite extension does not claim exact rich-payload instance identity, arbitrary nondeterministic projections, changing join keys, provider schedules or framework behavior. The earlier scalar and ordered-repair limits -remain. See [the high-review evidence](oracle-reviews/effect-startup-high-review.md). +remain. See the [Effect membership evidence](oracle-reviews/effect-startup-delta.md#membership-and-coalesced-source-writes). ### RC bound schema mutation receivers (2026-10-28) diff --git a/docs/contributing/oracle-reviews/effect-startup-delta.md b/docs/contributing/oracle-reviews/effect-startup-delta.md index dfb3b6c4ab..960de06889 100644 --- a/docs/contributing/oracle-reviews/effect-startup-delta.md +++ b/docs/contributing/oracle-reviews/effect-startup-delta.md @@ -209,7 +209,7 @@ No broader metadata or general delta-classifier closure follows. ## Subsequent prep-pr review Review of `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` found a newly exposed -factorial object-Set comparison path. The [Set-work repair](set-equality-work.md) +factorial object-Set comparison path. The [historical Set-work repair](https://github.com/TanStack/db/blob/0b47ee825aaa0c36b199a3c42652cab8d9672ec9/docs/contributing/oracle-reviews/set-equality-work.md) records its independent model, pre-edit RED, receiving paths, GREEN and mutants. The scalar startup owner's native model and histories still pass all 216 cases. Its separate opaque-key calibration repair is recorded below; startup and work @@ -236,8 +236,9 @@ assertion from the full repaired owner gives **214 passes and those same two failures**, with zero skipped tests. Cross-channel agreement therefore cannot substitute for continuity enforcement. -The repaired owner passes all **216 tests**. Combined equality, Collection/Effect -receiving, proxy-revert and startup checks pass **619/619**. All 5,512 startup +At that earlier head, the repaired owner passed all **216 tests**. Combined +equality, Collection/Effect receiving, proxy-revert and startup checks passed +**619/619**, including the later-removed Set work. All 5,512 startup reach cells still count 1; Set-work reach is unchanged. Targeted TypeScript, ESLint, oracle lint and formatting pass. Tests use at most two workers; no full build ran. Other applicable rubric outcomes above remain unchanged. @@ -254,3 +255,113 @@ Review identity is parent `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` plus that fingerprint and the Set-work fingerprints. The combined command is recorded in the Set-work review. Raw checker variants, reports and logs remain under `/private/tmp/effect-startup-delta/prep-pr/key-calibration/accepted-cr1/`. + +## Membership and coalesced source writes + +The high review of `d97c1aaf412661e8d5d3e0d528fe5c4667f69d65` exposed a +remaining membership distinction. An absent key can receive insert/delete/insert +in one flush and must enter. A present key receiving delete/insert/delete must +exit. Fresh callable and private-field projections also exposed a startup update +for an unmatched occurrence that never entered. Presence cannot be inferred from +payload equality or gross insert/delete counts. + +The existing owner adds 24 finite histories: scalar/callable/private payload, +one source/LEFT join, initially absent/present and separate/coalesced writes. +Independent source snapshots determine the whole expected delta at initial, +sequence, absent, reentry and next-use cuts. Both callback channels compare event +multiplicity, previous readable values and active opaque-key intervals. All 120 +new cells reach once. The original 200 histories and 5,512 reach cells stay. +The rich recorder preserves payload kind and readable result, without a claim +about exact re-evaluated instance identity. + +Before production edits, the expanded owner on unchanged `d97c1aaf4` failed +14 of these histories at delta/interval assertions. The complete recorded run +passed 631 tests and failed 16, including two separate Set stack probes. The +membership repair passes all 240 owner tests. Restoring the original classifier +fails 14 histories; inferring prior presence from net count alone fails all 24. +Public sync writes inside a real optimistic transaction's scheduler scope reach +the reported three-write histories. No production information is missing at +these observation cuts. + +Every Effect now retains its preceding coherent result, including a skipped +initial baseline. Prior membership plus net multiplicity decides membership +at the next cut. Only a continued row compares values to decide an update. +An Effect without update/batch handlers refreshes retained values without that +comparison or an update dispatch, preserving its later exit's value. Public +`EffectConfig`, `DeltaEvent` and the Effect guide supply the authority. + +The Effect production and owner files are unchanged from the verified subject +`375e9b26ca93b19cc266dd1ccdfcd10e97e820fc`. Their SHA256 values are: + +```text +6835de1fc044efec14f80ce7aa546451c9c1ce6134a1c5198779ee5ebd45c8b9 packages/db/src/query/effect.ts +469415454de90a75db84caaa5b6893ae8fb54a1f85536872070f9a3c54ded1a6 packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts +``` + +The [exact-head high-review record](https://github.com/TanStack/db/blob/0b47ee825aaa0c36b199a3c42652cab8d9672ec9/docs/contributing/oracle-reviews/effect-startup-high-review.md) +preserves all eight original findings, experiments, hostile variants, grammar +controls, rubric outcomes and reviewer assessment. That historical record's +647-test result includes the now-removed Set work. It is not the current count. +Current claims remain bounded to the declared eager controlled core histories. +Arbitrary provider/framework schedules, changing join keys/multiplicity and +exact rich-payload instance identity remain outside this instrument. + +## Maintainer scope revision, 2026-10-30 + +The maintainer explicitly removed Set performance work from this PR because no +application performance problem had been reported. The old Set matcher returns +byte-for-byte, together with the existing `utils.1`–`utils.40` owner and registry. +New Set models, receivers, properties 41/42, scale/work guards and the dedicated +Set report are removed. Existing laws, comparisons, reach and budgets remain. +The Set work's own new guards retire only with this explicit scope decision. +Their known synthetic limitations remain recorded under the primary deep-equality +owner in the coverage map; resumption requires a maintainer request or a real +workload. Historical generators and full traces remain in commit `0b47ee825` and +`/private/tmp/effect-startup-delta/high-review/`. + +The high-review ledger now has three Effect repairs (1, 2, 4), one accepted-design +semantic finding with a release-note correction (3), three explicitly deferred +Set findings (5–7), and one refuted coverage allegation (8). The original +factorial Set work is deferred with those findings. None is relabeled refuted or +closed by removing its test. The Effect membership repair, checker calibration, +callback comparison bypass and equal-projection suppression remain intact. + +### Validation after Set removal + +The six retained suites pass **581/581**, with zero failures or skipped tests. +They cover the existing change-event equality owner, ordinary utils tests, +proxy revert, ordinary Effects, Effect disposal and the Effect metadata owner. +All 240 metadata-owner tests pass. The original 5,512 reach cells and 120 added +membership cells each reach count 1. Targeted TypeScript, ESLint, oracle lint, +formatting and whitespace checks pass. + +The executed local configuration uses the installed dependencies, jsdom, +`tests/test-setup.ts`, a source alias for `@tanstack/db-ivm`, no coverage, and +at most two workers: + +```sh +node node_modules/vitest/vitest.mjs run \ + --config /private/tmp/effect-startup-delta/remove-set/focused.config.mjs \ + --reporter=default --reporter=json \ + --outputFile=/private/tmp/effect-startup-delta/remove-set/focused.json +``` + +The config, log and JSON report remain under that directory. The source alias +changes dependency resolution only. It does not replace the production driver +or independent comparison. The normal repository selectors name the same six +test files. Every `test:oracles` path exists after the removed receiver leaves +the script. + +The maintainer separately authorized `pnpm size:compare`, including its normal +package builds. Against merge base `bea84cd4554b7bd49df50cb0c569a34d8b2cd915`, +the typical app remains **96,732 bytes gzip** (0-byte change). The Effects app +changes from **99,303 to 99,340 bytes gzip** (+37 bytes). All other app scenarios +remain unchanged. The DB package adds 20 bytes gzip. The log is +`/private/tmp/effect-startup-delta/remove-set/size-compare.log`. + +These results supersede the earlier combined 619/647 counts for the current +scope. Those counts remain historical evidence for their stated subjects. +The Effect source and owner fingerprints above still identify the verified +implementation. Set production, its former `utils.1`–`utils.40` owner and its +registry are byte-identical to the merge base. No full repository test or build +ran beyond the separately authorized size comparison's normal package builds. diff --git a/docs/contributing/oracle-reviews/effect-startup-high-review.md b/docs/contributing/oracle-reviews/effect-startup-high-review.md deleted file mode 100644 index 81cd4966bc..0000000000 --- a/docs/contributing/oracle-reviews/effect-startup-high-review.md +++ /dev/null @@ -1,201 +0,0 @@ -# Effect and Set high-review evaluation - -## Subject and laws - -The supplied review targeted PR #2301 at exact head -`d97c1aaf412661e8d5d3e0d528fe5c4667f69d65`. It contained eight hypotheses, -removed duplicates, and supplied no executed reproduction. This record evaluates -all eight. The append-only raw ledger and full logs are retained under -`/private/tmp/effect-startup-delta/high-review/`. - -Effect callbacks describe result membership before and after a publication: -absent/present enters, present/absent exits, and changed present/present updates. -`EffectConfig`, `DeltaEvent`, and the Effects delta table in the live-query guide -supply the authority. Projection equality cannot establish prior membership. -Change-event equality intentionally permits plain/keyed-class compatibility; -a source edit outside the projected result need not produce an update. - -Set change-event equality requires a complete one-to-one assignment, including -plain/keyed-class overlap and homologous cycles. The glossary, `deepEquals`, and -the existing descriptor owner supply the authority. Matching many flat members -must return its equality judgment without consuming one JavaScript stack frame -per member. AGENTS.md's efficiency guidance also motivates removing avoidable -allocation and scanning; these measurements establish no latency SLA. - -## Experiments before production edits - -The whole-review plan grouped membership/value/work findings 1–4, Set assignment -and work findings 5–7, and generator-integrity finding 8. Production files were -byte-identical to the reviewed head until the extended oracle completed RED. - -The Effect owner adds 24 finite histories: scalar/callable/private-field payload, -one source/LEFT join, initially absent/present, and separate/coalesced source -transactions. Public sync writes inside a real optimistic transaction's scheduler -scope reconstruct both reported three-write sequences. Independent source -snapshots determine the expected delta. Both callback channels compare event -multiplicity, previous readable values, and active opaque-key intervals at -initial, sequence, absent, reentry and next-use cuts. The rich recorder preserves -payload kind and readable result; it makes no exact instance-identity claim. -The 120 new reach cells all execute once. Original 200 histories remain intact. - -The Set owner adds Gray-code descriptor chains. A supplied complete bijection -proves the positive judgment; an isolated column proves the negative judgment. -This small partial specification does not reproduce production's matching walk. -`utils.42` generates lengths 4–64, insertion order and both judgments in each -history, with the existing 100-run fixed/fresh campaign and replay interface. -Two finite 8,192-member cases run through the same model, driver and comparison. -An ordinary source getter and observer-only transforms measured work separately. -No framework/provider behavior is inferred from controlled eager sources. - -Unchanged production: **631 passed, 16 failed**, across seven targeted suites. -Fourteen Effect histories fail membership assertions; both finite Set witnesses -throw `RangeError: Maximum call stack size exceeded` during real comparison. -The original 216 Effect tests and other five neighboring suites pass. The -instrumented classifier sees balanced absent startup contributions and the -reported 2/1 and 1/2 insert/delete multiplicities on the real public path. -There is no missing-source-information hypothesis at these cuts. - -The exact seven-suite command, used for RED and GREEN, was: - -```sh -TANSTACK_DB_ORACLE_STATISTICS=1 node node_modules/vitest/vitest.mjs run \ - --config /private/tmp/effect-startup-delta/prep-pr/set-repair/final.config.mjs \ - --reporter=default --reporter=json \ - --outputFile=/private/tmp/effect-startup-delta/high-review/green-sparse-final.json -``` - -The scratch config selects utils, utils oracle, Set receivers, proxy revert, -Effect, Effect disposal and Effect metadata suites. It uses jsdom, the package -test setup, a source alias for `@tanstack/db-ivm`, two workers and no coverage. -The RED report is `extended-red-final.json`; the same selected paths and budgets -run in GREEN. Mutant configs differ only by the named source transform. -Replay adds `TANSTACK_DB_ORACLE_PROPERTY=utils.42`, -`TANSTACK_DB_ORACLE_SEED=-75265719`, `TANSTACK_DB_ORACLE_PATH=1`, and `-t '42:'`. - -## Repair and inference - -Every Effect now retains its preceding coherent result membership, including a -skipped initial baseline. Membership plus the net multiplicity decides enter, -exit or continued presence. Value equality decides only a continued row's -update. Retaining rows also preserves the previous callback value; this costs -one map entry and retained row per current result. There is no output Collection. -When neither update nor batch is observed, the runner refreshes retained values -without comparing or dispatching an update, preserving a later exit's value. - -Set matching now uses an explicit augmenting-path stack, cached candidate -comparisons and shared active ancestors. An aligned free diagonal needs one -candidate check. The final free column can be rejected immediately when isolated. -A displaced member tries the successor of its former assignment, then wraps -through every candidate. This retains complete reassignment rather than greedy -pairing. Edge storage is linear for aligned Sets and quadratic in the general -case; arbitrary nested graph complexity remains outside the work claim. - -The same seven suites now give **647 passed, zero failed**. All 120 new Effect -reach cells have count 1; the original 5,512 cells remain at count 1. Both Set -chain states reach 200 across the two campaigns; each large scale state reaches -once. Existing `utils.41` states remain 200 each and all 180 Set receiver cells -remain once. Targeted TypeScript, ESLint, changed-oracle lint, formatting and -whitespace checks pass. No full build, installation or lockfile edit occurred. - -Hostile experiments use Vite `enforce: pre` transforms, leaving checkout sources -unchanged: - -| Wrong design | Outcome at the intended boundary | -| --- | --- | -| Exact-head Effect classifier | 14 of 24 new histories fail delta/interval assertions; 10 pass. | -| Infer prior presence from net count alone | All 24 histories fail neighboring delta/previous-value comparisons. | -| Exact-head recursive Set matcher | Small campaigns pass; both 8,192-member comparisons overflow. This is a runtime failure after setup, not a value-assertion kill. | -| Greedy Set matcher with reassignment removed | All four `utils.41`/`utils.42` campaigns reject false inequality. | -| Direct captured replay | `utils.42`, seed `-75265719`, path `1`: one assertion failure under greedy matching, one pass under repaired production. Fixed campaign stands aside. | - -An initial transform experiment ran after TypeScript processing: whole-file -overlays failed setup, and a type-annotated greedy replacement did not install. -Those runs are retained and excluded from mutant evidence. The first iterative -breadth-first candidate was interrupted for excessive work on the finite scale -witness; no GREEN is attributed to it. The final depth-first candidate passes -without changing any reach, assertions or budgets. - -Work probes on identical inputs: - -| Input | Reviewed head | Repaired | -| --- | --- | --- | -| Effect replacement, enter/exit handlers only | 4 selected leaf reads | 2 reads; no update callback | -| Same replacement, update or batch handler | 4 reads | 4 reads; corresponding update delivered | -| Aligned object Sets, sizes 8/32/128/512 | 8/32/128/512 ancestor-map copies; 36/528/8,256/131,328 occupied-prefix checks | Zero map copies; 8/32/128/512 candidate calls | - -## Finding ledger and reviewer assessment - -Technical validity, PR action and durable value remain separate. Raw findings -retain their source-order IDs in the scratch ledger. - -| ID | Technical verdict and severity | PR action / durable value | -| --- | --- | --- | -| HIGH2301-1 | Confirmed, high: callable and opaque payloads expose the phantom startup update. Nondeterminism is unnecessary to establish the defect. | Fixed now; retain representation-independent membership histories. No general promise about nondeterministic projections is introduced. | -| HIGH2301-2 | Confirmed, high: both grouped three-write sequences emit update instead of enter/exit. Net count alone is insufficient. | Fixed now; retained membership plus net count, with separate/coalesced and next-use witnesses. | -| HIGH2301-3 | Described suppression is real; the correctness accusation conflicts with accepted projected-result/change-event equality semantics. | Accepted design for semantics. Changeset now explicitly documents general equal-projection suppression and class/plain compatibility remains tested. | -| HIGH2301-4 | Confirmed avoidable work, low; no measured latency consequence. | Fixed now; handler demand bypasses comparison while retained state stays current. Preserve the deterministic work probe. | -| HIGH2301-5 | Confirmed redundant per-pair map allocation, low. | Fixed now; share active ancestors, preserving cycle and assignment comparisons. | -| HIGH2301-6 | Confirmed quadratic occupied-prefix scanning for aligned Sets, medium. | Fixed now; direct diagonal path measured at one candidate call per member. No general linear-matching claim. | -| HIGH2301-7 | Confirmed flat-member stack overflow, medium. The reviewed code inherited this risk. | Fixed now; iterative reassignment with shared generated and finite scale judgments. Arbitrary nested depth is distinct. | -| HIGH2301-8 | Refuted as stated: ORC permits bounded exhaustive structural dimensions. The scalar arbitrary generates real variation. | No grammar removal. Same-arbitrary sample at fixed seed has 69 scalar values and both reversals; reach 200 never meant 200 distinct shapes. New structural-length coverage belongs to finding 7. | - -Loss audit: **8 = 6 fixed-now + 1 accepted-design + 1 refuted**. There are no -review-item deferrals, duplicate dispositions or known open violations in this -bounded repair. The documentation subclaim of item 3 is also repaired. There -are no newly confirmed sibling defects outside those IDs. This is a self-review; -no new independent reviewer was delegated. - -Reviewer recommendation: **lean hire for defect discovery, with verification -required before approval**. Prioritization was good: both leading hypotheses -reproduced through public APIs. Six useful engineering findings survived. The -report supplied no executed evidence, suggested net count without prior -membership, treated accepted equality as a correctness defect, and incorrectly -called finite enumeration pinned-only coverage. It is useful hypothesis input, -not sufficient PR approval evidence. - -## Oracle conformance and limits - -| Requirement | Evidence / applicability | -| --- | --- | -| ORC-001–003 | Accepted authority, explicit limits, independent snapshot/descriptor judgments and prose beside contract, grammar, driver and check. | -| ORC-004 | Reconstruction: both coalesced sequences and long chains. Ablation: representations expose equality-dependent startup; grouping exposes net membership; length exposes flat recursion; isolated column distinguishes false. Bounds are 24 finite histories, generated 4–64 and finite 8,192 members. Illegal source actions and unsupported non-witness descriptor graphs are excluded. | -| ORC-005–006 | Actual public callbacks/direct equality run before edits; both channels and future cuts are compared. Original and adjacent wrong implementations fail as classified above. | -| ORC-007 | `utils.42` runs identical fixed/fresh 100-run campaigns. Captured seed/path replay executes directly. Finite histories and scale cases are deliberately exhaustive/fixed, not random campaigns. | -| ORC-008–009 | Effect expected results remain stateless source recomputation; the callback ledger retains active opaque-key intervals only. Readable rich payload is an explicit abstraction of production values. Set descriptors represent independently authored member compatibility. | -| ORC-010 | Effect failures accumulate without stopping later cuts; shared cleanup preserves primary and secondary failures. Set witnesses own no external resources. Replay retains the same false-equality violation. | -| ORC-011 | No unresolved shared semantic-fault hypothesis: source recomputation differs from weighted deltas; supplied bijection/isolation differs from matching search. These formulations are directly exercised. | -| ORC-012–014 | This versioned record identifies subject, paths, bounds and mutants. Triggering and adjacent controls include balanced/unbalanced, present/absent, scalar/rich, assigned/isolated and separate/coalesced cases. No controlled premise is credited to a real external provider or framework. | -| ORC-015–016 | Reach counts above; fixed witnesses use shared model/driver/check, with no private expected answers or ordinary unit tests added to oracle files. | - -The law is encoded and asserted at every cut within the stated finite grammar. -Evidence does not prove arbitrary histories, changing join keys/multiplicity, -provider timing, framework callbacks, exact rich-payload instance identity, or -arbitrary cycle topology/nested-value depth. Existing ordered repair, scalar -metadata, Set receiving, draft comparison and disposal tests remain green. No -assertion, equality law, run budget or reach minimum was weakened. - -Whole-PR readiness is separate: the old 26 green checks cover `d97c1aaf4`. -Final-head remote CI and review must cover the new patch before merge readiness -can be claimed. No merge is authorized by this evaluation. - -## Reviewed source fingerprints - -These SHA256 values identify the repaired executable subject against parent -`d97c1aaf412661e8d5d3e0d528fe5c4667f69d65`. They exclude this evidence record. - -```text -6835de1fc044efec14f80ce7aa546451c9c1ce6134a1c5198779ee5ebd45c8b9 packages/db/src/query/effect.ts -1cad064d83d744c0616fe84066b19b753fbad00d543b69f6944bfb06de2f0ff7 packages/db/src/utils.ts -469415454de90a75db84caaa5b6893ae8fb54a1f85536872070f9a3c54ded1a6 packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts -6203a3d9bd5663a5ee7c14b5c66e0e139bbf7fe2d9afb09650cd01e31c1760f1 packages/db/tests/set-equality-oracle.ts -c84a3d24f2ed2cbb251c5ea758c36a36234fef18e09d82a608929d696e1d6816 packages/db/tests/utils-oracle.property.test.ts -043e546f9a2bf3ba52395c8c9d203456bf63f4fa481b0b4883fe4a56b24f448e packages/db/tests/oracle-config.ts -``` - -## Exact reviewed repair commit - -The executable subject is `375e9b26ca93b19cc266dd1ccdfcd10e97e820fc`. -The source fingerprints above match that commit after the pre-commit hooks. -This appended entry binds the 647-test GREEN, 22 diagnostic passes, targeted -type/lint checks and eight-item audit to that exact subject. The following -documentation commit adds only this binding, without changing executable code. diff --git a/docs/contributing/oracle-reviews/set-equality-work.md b/docs/contributing/oracle-reviews/set-equality-work.md deleted file mode 100644 index 21a6a09209..0000000000 --- a/docs/contributing/oracle-reviews/set-equality-work.md +++ /dev/null @@ -1,166 +0,0 @@ -# Object-Set equality work in Effect publication - -## Law and repair boundary - -PR #2301 makes unordered Effects compare old and new projected values. Review -of head `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` found that the existing -object-Set matcher retries permutations when several members have equal contents -and another member differs. A scalar source row can project those Sets, so this -new Effect cost does not already occur in source-Collection comparison. - -Preserve change-event equality: Set order does not matter, each member matches -once, and distinct objects with equal contents retain their multiplicity. -The glossary and `deepEquals` API supply the authority. A keyed class matches -a plain record with equal fields, but two different classes differ. Matching -must permit reassignment; greedy pairing changes the accepted law. - -The maintainer accepted this performance repair after independent review. -Its engineering requirement removes factorial assignment work under AGENTS.md's -algorithm-efficiency guidance, without adding a numerical latency API promise. -Flat one-field members permit at most `2*n*m` leaf reads per direct comparison. -Each core one-row replacement permits `16*(n+1)^2` reads, allowing repeated -comparisons at fixed bookkeeping boundaries. Both bounds were stated before -changing production. They do not promise quadratic total runtime or bound -arbitrary graph complexity. - -## Instrument and pre-edit gate - -The primary `utils-oracle.property.test.ts` owner imports the descriptor judgment -in `set-equality-oracle.ts`. That model enumerates compatible assignments with -memoized subsets. It neither reads production objects nor uses production -equality. Production instead finds augmenting paths through cached candidates. -Each candidate starts with the same active ancestors; recursive comparisons -remove their own pairs before returning. Another member's assignment therefore -does not change a cached candidate's context. - -The generated family exhausts ordered two-member words over plain records and -two keyed classes in five homologous shapes: flat, self-cycle, owner-cycle, -nested owner-cycle, and shared acyclic leaf. Payloads range from -100 to 100. -Right-word reversal repeats already exhausted permutations; it adds no claimed -class case. Flat duplicate families use sizes 1, 2, 4, 6 and 8 with equal copies, -one late changed leaf, and different cardinality. Removing duplicate values -loses the performance witness; removing class/plain overlap loses the greedy -counterexample; removing ancestor shapes loses cycle-context evidence. -Members are distinct objects. Repeated references would collapse in a real Set -and are excluded from this multiplicity grammar. - -The appended property is `utils.41`; selectors 1–40 stay unchanged. Fixed seed -20260911 and a fresh seed each run 100 instances of the same property and budget. -The extension `query/set-equality-receivers-oracle.test.ts` reuses that equality -judgment for 20 finite histories on each of three actual core paths: Collection -change events, unordered Effects and ordered Effects. Effect sources contain -only scalar `id` and `tick` fields; their projection constructs the Set. -Equal, reordered, late-mismatch and changed-multiplicity replacements are checked -at startup, replacement and next use. Event multiplicity, selected member values, -row ID and previous values remain observable. - -Before production edits, the extended owners had **134 passes and 11 work -assertion failures**: two generated campaigns and nine receiving histories. -Returned equality and public deltas remained correct. Four flat members needed -128 reads against a bound of 32. At eight members each late-mismatch receiver -needed 219,200 reads against 1,296. The neighboring utils, proxy-revert, Effect -and startup suites passed **474/474**, including all 216 startup-owner tests, -before the matcher changed. - -An initial run exposed a missing replay-registry entry, then registration order -shifted existing selectors. Both instrument issues were repaired before final -RED; neither setup failure is product evidence. Two test-only generic/union -annotations were also corrected after targeted type checking. - -## GREEN, calibration, and replay - -The matcher caches each candidate result and reassigns existing matches when -necessary. Unused candidates are preferred to retain the inexpensive aligned -equal-Set path. Draft and persisted-snapshot comparisons retain their ordered -branch. No startup predicate, assertion, exclusion or existing budget changed. - -| Experiment | Result | -| --- | --- | -| Extended direct and receiving owners | 145 passed | -| Combined equality, receiver and neighboring run | 619 passed, zero skipped | -| Original matcher restored as mutant | 134 passed, 11 intended work failures | -| Greedy matcher without reassignment | 143 passed, 2 equality assertion failures | -| Original-mutant direct replay | One work failure; seed -310527886, path `0:0` | - -Each of eight new direct reach states counts 200 in GREEN. Each of 180 finite -receiver/path/history/cut cells counts 1. The unchanged scalar startup owner -retains its 5,512 cells, each count 1. The greedy mutant fails class/plain -assignment; missing later reach follows that early semantic failure and is not -another claimed mutant kill. - -The guarded replay-witness channel records one failing `utils.41` replay. -The same requested replay passes once on repaired production. -The repaired public probe measures 30 reads at eight members, versus 219,200 -before repair. Its plain ten-member replacement measured 0.081 ms, versus -1,859.882 ms before repair on this machine. These single local elapsed samples -are diagnostic; no test asserts timing. - -Tests use Node 24.19.0 and installed Vitest 3.2.4, at most two workers, with -coverage disabled. Targeted TypeScript checking passes. No install, lockfile -change or full build was required. Raw configs, reports, replay witness, -subjects and logs remain under -`/private/tmp/effect-startup-delta/prep-pr/set-repair/`. - -The ordinary package command, from the repository root: - -```sh -pnpm --filter @tanstack/db exec vitest run --coverage.enabled=false \ - --maxWorkers=2 tests/utils-oracle.property.test.ts \ - tests/query/set-equality-receivers-oracle.test.ts tests/utils.test.ts \ - tests/proxy-revert-oracle.property.test.ts tests/effect.test.ts \ - tests/effect-disposal-oracle.test.ts \ - tests/query/virtual-row-fields-effect-oracle.test.ts -``` - -Replay uses `TANSTACK_DB_ORACLE_PROPERTY=utils.41`, -`TANSTACK_DB_ORACLE_SEED=-310527886` and `TANSTACK_DB_ORACLE_PATH=0:0`, selecting -`41:`. Full finite receiving execution remains independent of seed replay. - -## Rubric review and limits - -This table is the author's self-review. A fresh independent reviewer also -inspected the matcher, model, driver and raw evidence and found no supported -implementation defect. That review ran no tests. Its redundant-reversal -observation is accounted for above; reversal adds no independent class coverage. - -| Requirement | Evidence | -| --- | --- | -| ORC-001 | Accepted equality authority and separate engineering work bounds are stated above. | -| ORC-002 | Authored descriptors supply expected equality independently of production. | -| ORC-003 | Literate shared model and receiving drivers state law, grammar, observations and limits. | -| ORC-004 | Reconstruction, ablations, bounds, reversal redundancy and duplicate-reference exclusion are stated above. | -| ORC-005 | Direct helper, public Collection changes and both core Effect paths compare at named cuts. Scalar sources isolate projected-value work. | -| ORC-006 | Original and greedy mutants fail the intended work and equality assertions. | -| ORC-007 | Same property and 100-run budget in fixed/fresh campaigns; one-run seed/path replay executed. | -| ORC-008 | No product lifecycle state added; the used-member subset enforces one-to-one assignment. | -| ORC-009 | Descriptors abstract authored values/prototypes and homologous links; cuts use glossary terms. | -| ORC-010 | Receivers retain causal failures at each cut, continue to next use and preserve cleanup errors. | -| ORC-011 | Subset enumeration challenges augmenting paths on non-equivalence class compatibility; arbitrary topology remains outside the model. | -| ORC-012 | Baseline identity, fingerprints, experiments and limits accompany the repair. | -| ORC-013 | Equal/reordered copies distinguish changed leaves and multiplicity; class/plain words reject greedy matching. | -| ORC-014 | Claims stop at eager controlled core sources; no provider/framework handoff is inferred. | -| ORC-015 | Eight direct states count 200; all 180 receiving cells count 1 in GREEN. | -| ORC-016 | All histories and receiving paths share the descriptor judgment. | - -The work claim covers flat members at the stated sizes and one-row eager core -receivers. It does not bound arbitrary recursive graphs, nested-Set complexity, -changing getters, proxies, large Sets, real providers/frameworks, or every -comparator caller's total work. Class/cycle evidence covers two-member words -with matching link shapes, not general graph isomorphism or all cross-member -topologies. Existing broader equality laws remain with their owners. -Fixture-construction exceptions precede the cleanup wrapper; those setup -failures are outside the cleanup evidence. -The receiving ledger observes batch delivery. Rich named-callback metadata and -opaque-key calibration remain with the Effect metadata owner. Its separate -[continuity calibration repair](effect-startup-delta.md#key-interval-checker-calibration) -does not extend this Set-work observation boundary. - -Reviewed identity is parent `a7ea76cddf7ab2fe03f49d84c6d596dd598fedba` plus -these SHA256 fingerprints: - -| File | SHA256 | -| --- | --- | -| `packages/db/src/utils.ts` | `6ee964e4cd4ef93131ce5d287dc5910cbfc218dc62cf65ee2c8897eb12d1ef28` | -| `packages/db/tests/set-equality-oracle.ts` | `d2654eb35cb3d86c9aefd7969afdb68d7ae230138da93a0c43abd89efcf592b2` | -| `packages/db/tests/utils-oracle.property.test.ts` | `5349e7dd770d73058aaf7d07f83fd81a0873a5f15e8a995b89d1a65eaa68acbb` | -| `packages/db/tests/query/set-equality-receivers-oracle.test.ts` | `1532a4b14b64503fd8dc032b73c5b82f1563547b9419fd24ad9c2a809790e9db` | diff --git a/packages/db/package.json b/packages/db/package.json index 396bb427f4..1b8a299d76 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -23,7 +23,7 @@ "test": "vitest --run", "test:dist": "vitest run --config vitest.dist.config.ts", "test:facade-retention": "node --expose-gc --import tsx tests/facade-retention.probe.ts", - "test:oracles": "vitest --run --coverage.enabled=false tests/reverse-index-oracle.property.test.ts tests/query/compiler/in-evaluator-oracle.property.test.ts tests/cleanup-queue-oracle.property.test.ts tests/comparison-oracle.property.test.ts tests/duration-query-equality-oracle.test.ts tests/cursor-oracle.property.test.ts tests/index-update-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/change-event-history-oracle.test.ts tests/index-suggestion-oracle.test.ts tests/btree-map-oracle.test.ts tests/paced-mutations-oracle.test.ts tests/db-client-hydration-authority-oracle.test.ts tests/local-storage-order-oracle.test.ts tests/local-storage-value-fidelity-oracle.property.test.ts tests/collection-cleanup-restart-oracle.test.ts tests/collection-gc-transaction-oracle.property.test.ts tests/collection-gc-retention-oracle.property.test.ts tests/collection-gc-mutation-oracle.property.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/transaction-ownership-oracle.property.test.ts tests/optimistic-settlement-boundaries-oracle.test.ts tests/optimistic-history-publication-oracle.test.ts tests/optimistic-history-outcomes-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-index-value-oracle.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history-oracle.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-reentrancy-oracle.test.ts tests/collection-subscription-lifecycle-publication-oracle.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-cache-options-oracle.test.ts tests/mutation-confirmation-oracle.test.ts tests/mutation-handler-types-oracle.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/identity-output-shape-oracle.test.ts tests/query/optimizer-semantics-oracle.test.ts tests/query/index-path-collision-oracle.test.ts tests/query/custom-index-order-selection-oracle.test.ts tests/query/mixed-index-order-oracle.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-alias-shadowing-oracle.test.ts tests/query/includes-outer-join-route-work-oracle.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/subquery-user-value-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state-oracle.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts tests/query/virtual-row-fields-oracle.test.ts tests/query/virtual-row-fields-effect-oracle.test.ts tests/query/set-equality-receivers-oracle.test.ts tests/query/virtual-row-fields-types-oracle.test.ts tests/query/where-predicate-publication-oracle.property.test.ts tests/query/join-result-key-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/live-query-deferred-acquisition-oracle.test.ts tests/flat-change-tracking-oracle.property.test.ts tests/truncate-readiness-oracle.test.ts tests/proxy-detachment-contract-oracle.test.ts tests/production-error-messages-oracle.test.ts tests/query/bucket-facade-rollback-oracle.property.test.ts tests/query/aggregate-select-oracle.property.test.ts tests/query/load-subset-joint-replay-oracle.property.test.ts", + "test:oracles": "vitest --run --coverage.enabled=false tests/reverse-index-oracle.property.test.ts tests/query/compiler/in-evaluator-oracle.property.test.ts tests/cleanup-queue-oracle.property.test.ts tests/comparison-oracle.property.test.ts tests/duration-query-equality-oracle.test.ts tests/cursor-oracle.property.test.ts tests/index-update-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/change-event-history-oracle.test.ts tests/index-suggestion-oracle.test.ts tests/btree-map-oracle.test.ts tests/paced-mutations-oracle.test.ts tests/db-client-hydration-authority-oracle.test.ts tests/local-storage-order-oracle.test.ts tests/local-storage-value-fidelity-oracle.property.test.ts tests/collection-cleanup-restart-oracle.test.ts tests/collection-gc-transaction-oracle.property.test.ts tests/collection-gc-retention-oracle.property.test.ts tests/collection-gc-mutation-oracle.property.test.ts tests/effect-disposal-oracle.test.ts tests/optimistic-transaction-oracle.property.test.ts tests/transaction-ownership-oracle.property.test.ts tests/optimistic-settlement-boundaries-oracle.test.ts tests/optimistic-history-publication-oracle.test.ts tests/optimistic-history-outcomes-oracle.test.ts tests/collection-metadata-publication-oracle.property.test.ts tests/collection-index-value-oracle.test.ts tests/collection-state-retention-oracle.property.test.ts tests/collection-subscription-lifecycle-history-oracle.property.test.ts tests/collection-subscription-lifecycle-oracle.test.ts tests/collection-subscription-reentrancy-oracle.test.ts tests/collection-subscription-lifecycle-publication-oracle.property.test.ts tests/collection-subscription-replay-oracle.property.test.ts tests/d2-source-reconciliation-oracle.property.test.ts tests/live-query-cache-options-oracle.test.ts tests/mutation-confirmation-oracle.test.ts tests/mutation-handler-types-oracle.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/query/cold-join-reconciliation-oracle.test.ts tests/query/identity-output-shape-oracle.test.ts tests/query/optimizer-semantics-oracle.test.ts tests/query/index-path-collision-oracle.test.ts tests/query/custom-index-order-selection-oracle.test.ts tests/query/mixed-index-order-oracle.test.ts tests/query/includes-collection-oracle.property.test.ts tests/query/includes-functional-projection-oracle.test.ts tests/query/includes-functional-input-boundary.test.ts tests/query/includes-context-transport-oracle.test.ts tests/query/includes-cross-formulation-oracle.property.test.ts tests/query/includes-optimistic-oracle.property.test.ts tests/query/includes-oracle.property.test.ts tests/query/includes-alias-shadowing-oracle.test.ts tests/query/includes-outer-join-route-work-oracle.test.ts tests/query/includes-publication-oracle.test.ts tests/query/includes-query-shape-oracle.test.ts tests/query/subquery-user-value-oracle.test.ts tests/query/includes-temporal-oracle.test.ts tests/query/includes-work-counter-oracle.test.ts tests/query/load-subset-oracle.property.test.ts tests/query/load-subset-replay-refinement-oracle.test.ts tests/query/load-subset-source-readiness-refinement-oracle.test.ts tests/query/load-subset-transaction-refinement-oracle.test.ts tests/query/ordered-source-loader-state-oracle.test.ts tests/query/ordered-demand-retirement.test.ts tests/query/ordered-default-work.test.ts tests/query/ordered-lifecycle-oracle.property.test.ts tests/query/ordered-work-oracle.property.test.ts tests/query/pagination-oracle.property.test.ts tests/query/includes-space-oracle.test.ts tests/query/virtual-row-fields-oracle.test.ts tests/query/virtual-row-fields-effect-oracle.test.ts tests/query/virtual-row-fields-types-oracle.test.ts tests/query/where-predicate-publication-oracle.property.test.ts tests/query/join-result-key-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/live-query-deferred-acquisition-oracle.test.ts tests/flat-change-tracking-oracle.property.test.ts tests/truncate-readiness-oracle.test.ts tests/proxy-detachment-contract-oracle.test.ts tests/production-error-messages-oracle.test.ts tests/query/bucket-facade-rollback-oracle.property.test.ts tests/query/aggregate-select-oracle.property.test.ts tests/query/load-subset-joint-replay-oracle.property.test.ts", "bench:nested-includes": "vitest bench tests/query/includes-performance.bench.ts --run", "test:indexed-db:package": "pnpm --filter @tanstack/db... build && vitest run --config vitest.indexed-db.package.config.ts", "test:collection-gc:browser": "playwright test --config playwright.collection-gc.config.ts", diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts index 7a5cc3983f..5c973f7627 100644 --- a/packages/db/src/utils.ts +++ b/packages/db/src/utils.ts @@ -194,86 +194,41 @@ export function deepEqualsInternal( return aValues.every((val) => b.has(val)) } - // Candidate comparisons share only the active ancestors. Each recursive - // comparison removes its own pairs, so an edge's result is independent of - // other assignments. Cache edges and reassign matches instead of retrying - // permutations. Plain records can match distinct keyed classes, so a - // greedy first match alone would reject some equal Sets. - // Cache diagonal comparisons separately: aligned Sets need linear storage - // and no candidate scan. Other rows cache only the pairs actually tried. - // Zero is unknown, one is unequal, and two is equal. - const diagonal = new Uint8Array(aValues.length) - const edges: Array | undefined> = [] - const equalAt = (left: number, right: number): boolean => { - if (left === right && diagonal[left] !== 0) return diagonal[left] === 2 - const row = - left === right - ? undefined - : (edges[left] ??= new Map()) - const cached = row?.get(right) - if (cached !== undefined) return cached - const equal = deepEqualsInternal( - aValues[left], - bValues[right], - visited, - draft, - persistedSnapshot, - ) - if (left === right) diagonal[left] = equal ? 2 : 1 - else row!.set(right, equal) - return equal - } - const matchedRight = bValues.map(() => -1) - const matchedLeft = aValues.map(() => -1) - const matchValue = (index: number): boolean => { - if (matchedRight[index] === -1 && equalAt(index, index)) { - matchedRight[index] = index - matchedLeft[index] = index - return true - } - // An isolated final column cannot participate in any complete assignment. - // Check it directly rather than rediscovering that through every path. - if (index === aValues.length - 1) { - const free = matchedRight.indexOf(-1) - if (aValues.every((_, left) => !equalAt(left, free))) return false - } - // Find an augmenting path without using JavaScript call depth for the - // number of members. Each right predecessor names the left that found it. - const predecessors = new Int32Array(bValues.length).fill(-1) - const stack = [{ left: index, next: 0 }] - while (stack.length > 0) { - const frame = stack[stack.length - 1]! - if (frame.next === bValues.length) { - stack.pop() - continue - } - // After displacement, try the successor of the former assignment - // first. Wrap once so every candidate remains eligible. - const start = - matchedLeft[frame.left] === -1 - ? frame.left - : matchedLeft[frame.left]! + 1 - const right = (start + frame.next++) % bValues.length - if (predecessors[right] !== -1 || !equalAt(frame.left, right)) continue - predecessors[right] = frame.left - if (matchedRight[right] !== -1) { - stack.push({ left: matchedRight[right]!, next: 0 }) - continue - } - // Flip every edge back to the root, releasing each old assignment. - let slot = right - while (slot !== -1) { - const owner = predecessors[slot]! - const previous = matchedLeft[owner]! - matchedRight[slot] = owner - matchedLeft[owner] = slot - slot = previous + // Object-valued Sets are unordered. Match each value once, carrying a + // branch-local cycle map so a failed candidate cannot poison the next. + const matchValues = ( + index: number, + remaining: ReadonlyArray, + branchVisited: Map, + ): boolean => { + if (index === aValues.length) return true + const value = aValues[index] + for (const [remainingIndex, candidateIndex] of remaining.entries()) { + const candidateVisited = new Map(branchVisited) + if ( + deepEqualsInternal( + value, + bValues[candidateIndex], + candidateVisited, + draft, + persistedSnapshot, + ) && + matchValues( + index + 1, + remaining.filter((_, otherIndex) => otherIndex !== remainingIndex), + candidateVisited, + ) + ) { + return true } - return true } return false } - const result = aValues.every((_, index) => matchValue(index)) + const result = matchValues( + 0, + bValues.map((_, index) => index), + visited, + ) visited.delete(a) return result } diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index f61d2d1dba..ca9c1a8927 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -358,7 +358,7 @@ const publicationProperties = [ // utils-oracle and comparison-oracle number their properties in file order. const utilsProperties = Array.from( - { length: 42 }, + { length: 40 }, (_, index) => `utils.${index + 1}`, ) const comparisonProperties = Array.from( diff --git a/packages/db/tests/query/set-equality-receivers-oracle.test.ts b/packages/db/tests/query/set-equality-receivers-oracle.test.ts deleted file mode 100644 index 63ddc6659f..0000000000 --- a/packages/db/tests/query/set-equality-receivers-oracle.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Core change-event consumers must preserve unordered Set equality without - * permutation work. The primary utils equality owner supplies the descriptor - * model. These receivers check Collection changes and ordered/unordered Effect - * callbacks at startup, replacement and next use. Scalar-only Effect sources - * isolate comparison of the projected Set from source-Collection comparison. - * - * Authority: change-event equality in the glossary, EffectConfig's complete - * deltas and DeltaEvent.previousValue. The quadratic leaf-read bound is an - * engineering guard for this accepted performance repair, not a latency SLA. - * Eager controlled core sources supply the premise. No provider, framework, - * arbitrary scheduling, general graph-work or callback-count guarantee follows. - */ -import { afterAll, expect, test } from 'vitest' -import { createCollection } from '../../src/collection/index.js' -import { createEffect } from '../../src/query/effect.js' -import { declareOracle } from '../oracle-declaration.js' -import { createOracleReach } from '../oracle-reach.js' -import { - authoredSet, - expectedSetEquality, - plainSetSpecs, - setWorkSizes, -} from '../set-equality-oracle.js' -import { mockSyncCollectionOptions, withOracleCleanup } from '../utils.js' - -type Row = { id: number; members: Set<{ value: number }> } -type Event = { type: string; value: Row; previousValue?: Row } -const paths = [`collection`, `unordered-effect`, `ordered-effect`] as const -const modes = [`equal`, `reordered`, `late-mismatch`, `multiplicity`] as const -const histories = setWorkSizes.flatMap((size) => - modes.map((mode) => ({ size, mode })), -) -const cuts = [`initial`, `replacement`, `next-use`] as const -const receivingReach = createOracleReach( - `Set equality core receivers`, - paths.flatMap((path) => - histories.flatMap(({ size, mode }) => - cuts.map((cut) => `${path}/${size}/${mode}/${cut}`), - ), - ), - { replaying: false }, -) -let entered = false -afterAll(() => { - if (entered) receivingReach.assertReached() - if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) - console.info( - `Set equality receiver reach`, - Object.fromEntries(receivingReach.counts()), - ) -}) - -function valuesAt( - history: (typeof histories)[number], - tick: number, -): Array { - const { size, mode } = history - if (tick === 2) return Array.from({ length: size }, () => 3) - if (mode === `reordered`) { - const values = Array.from({ length: size }, (_, index) => index % 2) - return tick === 0 ? values : values.reverse() - } - const values = Array.from({ length: size }, () => 0) - if (mode === `late-mismatch` && tick === 0) values[size - 1] = 1 - if (mode === `multiplicity`) { - values[size - 1] = 1 - if (tick === 1) values[0] = 1 - } - return values -} - -// Callback values remain separate immutable authored objects. Normalize only -// Set iteration order; preserve all event multiplicity and previous values. -function checkSetDelta( - events: ReadonlyArray, - before: ReadonlyArray | undefined, - after: ReadonlyArray, - path: (typeof paths)[number], -): void { - const row = (values: ReadonlyArray) => ({ - id: 1, - members: [...values].sort((a, b) => a - b), - }) - const observedRow = (value: Row) => ({ - id: value.id, - members: [...value.members] - .map((member) => member.value) - .sort((a, b) => a - b), - }) - const changed = - before !== undefined && - !expectedSetEquality(plainSetSpecs(before), plainSetSpecs(after)) - const expected = - before === undefined - ? [ - { - type: path === `collection` ? `insert` : `enter`, - value: row(after), - }, - ] - : changed - ? [{ type: `update`, value: row(after), previousValue: row(before) }] - : [] - expect( - events.map((event) => ({ - type: event.type, - value: observedRow(event.value), - ...(event.previousValue - ? { - previousValue: observedRow(event.previousValue), - } - : {}), - })), - ).toStrictEqual(expected) -} -declareOracle({ - model: [expectedSetEquality], - check: [checkSetDelta], - grammar: [histories], -}) - -for (const path of paths) { - test.each(histories)( - `${path} preserves Set equality and bounded work: %j`, - async (history) => { - entered = true - receivingReach.beginHistory() - let reads = 0 - const members = (tick: number) => - authoredSet( - plainSetSpecs(valuesAt(history, tick)), - `flat`, - () => reads++, - ) - const events: Array = [] - const scalarOptions = mockSyncCollectionOptions({ - id: `set-scalar-${path}-${history.size}-${history.mode}`, - getKey: (row: { id: number; tick: number }) => row.id, - initialData: [{ id: 1, tick: 0 }], - }) - const scalar = createCollection(scalarOptions) - const setOptions = mockSyncCollectionOptions({ - id: `set-row-${path}-${history.size}-${history.mode}`, - getKey: (row: Row) => row.id, - initialData: [{ id: 1, members: members(0) }], - }) - const source = - path === `collection` ? createCollection(setOptions) : undefined - const subscription = source?.subscribeChanges( - (changes) => events.push(...changes), - { includeInitialState: true }, - ) - const effect = - path === `collection` - ? undefined - : createEffect({ - query: (q) => { - const query = q.from({ row: scalar }) - const selected = - path === `ordered-effect` - ? query.orderBy(({ row }) => row.id) - : query - return selected.fn.select(({ row }) => ({ - id: row.id, - members: members(row.tick), - })) - }, - onBatch: (batch) => { - events.push(...batch) - }, - }) - const failures: Array = [] - await withOracleCleanup(async () => { - // A host task turn delivers queued Effect callbacks, as in the existing - // metadata owner. No provider completion is inferred from this drain. - let before: Array | undefined - for (const [tick, cut] of cuts.entries()) { - if (tick > 0) events.length = 0 - reads = 0 - if (tick > 0) { - if (source) { - setOptions.utils.begin() - setOptions.utils.write({ - type: `update`, - value: { id: 1, members: members(tick) }, - }) - setOptions.utils.commit() - } else { - scalarOptions.utils.begin() - scalarOptions.utils.write({ - type: `update`, - value: { id: 1, tick }, - }) - scalarOptions.utils.commit() - } - } - await new Promise((resolve) => setTimeout(resolve, 0)) - const work = reads - const after = valuesAt(history, tick) - receivingReach.reach(`${path}/${history.size}/${history.mode}/${cut}`) - try { - checkSetDelta(events, before, after, path) - if (tick > 0) - expect(work, `quadratic receiver leaf reads`).toBeLessThanOrEqual( - 16 * (history.size + 1) ** 2, - ) - } catch (error) { - failures.push( - new Error( - `${cut}: ${error instanceof Error ? error.message : String(error)}`, - { cause: error }, - ), - ) - } - before = after - } - if (failures.length) - throw new AggregateError( - failures, - failures.map((error) => error.message).join(`\n`), - { - cause: failures[0], - }, - ) - }, [ - async () => { - await effect?.dispose() - }, - () => subscription?.unsubscribe(), - async () => { - await source?.cleanup() - }, - async () => { - await scalar.cleanup() - }, - ]) - }, - ) -} diff --git a/packages/db/tests/set-equality-oracle.ts b/packages/db/tests/set-equality-oracle.ts deleted file mode 100644 index 2a0dd62dfe..0000000000 --- a/packages/db/tests/set-equality-oracle.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Change-event equality matches unordered Set members once each. Distinct - * objects can have the same value, so their multiplicity still matters. - * Authority: deepEquals's supported Set domain and the glossary's class/plain - * compatibility rule. A plain record can match either keyed class; two - * different classes cannot match. Greedy matching therefore is insufficient. - * - * The model enumerates assignments of authored descriptors. It never reads - * production objects or calls the production comparator. Homologous self and - * ancestor cycles exercise the same member law; arbitrary graph isomorphism, - * changing getters, proxies and mutation during comparison are excluded. - * Pure getters count leaf reads. The accepted performance repair requires - * quadratic work for flat one-field members, not a numerical latency promise. - */ -import { expect } from 'vitest' -import { deepEquals } from '../src/utils.js' -import { declareOracle } from './oracle-declaration.js' - -export type SetMemberSpec = { - kind: `plain` | `first` | `second` - value: number -} -export type SetShape = `flat` | `self` | `owner` | `nested-owner` | `shared` -type Member = { value: number; next?: unknown } -type Equal = (left: unknown, right: unknown) => boolean - -class FirstMember { - constructor(public value: number) {} -} -class SecondMember { - constructor(public value: number) {} -} - -export function expectedSetEquality( - left: ReadonlyArray, - right: ReadonlyArray, -): boolean { - if (left.length !== right.length) return false - // At most eight members in this grammar. Memoized subset enumeration is - // an independent, small specification, not the production matching walk. - const answers = new Map() - const assign = (index: number, used: number): boolean => { - if (index === left.length) return true - const known = answers.get(used) - if (known !== undefined) return known - const member = left[index]! - const result = right.some( - (candidate, slot) => - (used & (1 << slot)) === 0 && - member.value === candidate.value && - (member.kind === `plain` || - candidate.kind === `plain` || - member.kind === candidate.kind) && - assign(index + 1, used | (1 << slot)), - ) - answers.set(used, result) - return result - } - return assign(0, 0) -} - -// The driver allocates every Set member separately, including repeated values. -// Only the declared shared-leaf shape aliases an acyclic child within one Set. -export function authoredSet( - specs: ReadonlyArray, - shape: SetShape, - read: () => void, -): Set { - const set = new Set() - const shared = { leaf: 1 } - for (const spec of specs) { - const member: Member = - spec.kind === `first` - ? new FirstMember(spec.value) - : spec.kind === `second` - ? new SecondMember(spec.value) - : { value: spec.value } - Object.defineProperty(member, `value`, { - enumerable: true, - get: () => { - read() - return spec.value - }, - }) - if (shape === `self`) member.next = member - if (shape === `owner`) member.next = set - if (shape === `nested-owner`) member.next = new Set([set]) - if (shape === `shared`) member.next = shared - set.add(member) - } - return set -} - -export function plainSetSpecs( - values: ReadonlyArray, -): Array { - return values.map((value) => ({ kind: `plain`, value })) -} - -export function checkSetEquality( - left: ReadonlyArray, - right: ReadonlyArray, - shape: SetShape, - equal: Equal = deepEquals, -): void { - let reads = 0 - const a = authoredSet(left, shape, () => reads++) - const b = authoredSet(right, shape, () => reads++) - const expected = expectedSetEquality(left, right) - for (const [from, to] of [ - [a, b], - [b, a], - ] as const) { - reads = 0 - const actual = equal(from, to) - expect(actual, `Set member assignment (${shape})`).toBe(expected) - // Flat members have one numeric field. At most one comparison per member - // pair needs two leaf reads. Cardinality rejection needs none. - if (shape === `flat`) - expect(reads, `quadratic Set member reads`).toBeLessThanOrEqual( - 2 * left.length * right.length, - ) - } -} - -export const setShapes: ReadonlyArray = [ - `flat`, - `self`, - `owner`, - `nested-owner`, - `shared`, -] -export const setWorkSizes = [1, 2, 4, 6, 8] as const -export const setMemberKinds: ReadonlyArray = [ - `plain`, - `first`, - `second`, -] - -// Two-member class words exhaust the smallest non-equivalence matching domain. -// In particular [plain, first] vs [first, second] requires reassignment. -export function checkSetFamily( - value: number, - reverse: boolean, - reach: ( - state: SetShape | `equal-duplicates` | `late-mismatch` | `cardinality`, - ) => void, -): void { - const words = setMemberKinds.flatMap((first) => - setMemberKinds.map((second) => [first, second]), - ) - for (const shape of setShapes) { - for (const left of words) { - for (const right of words) { - const specs = (kinds: typeof left) => - kinds.map((kind) => ({ kind, value })) - const a = specs(left) - const b = specs(reverse ? [...right].reverse() : right) - checkSetEquality(a, b, shape) - } - } - reach(shape) - } - // Nearby equal, changed-leaf and changed-multiplicity values all use the - // same assignment model. Late mismatch reconstructs the reported slowdown. - for (const size of setWorkSizes) { - const same = Array.from({ length: size }, () => value) - const different = [...same.slice(1), value + 1] - checkSetEquality( - plainSetSpecs(same), - plainSetSpecs([...same].reverse()), - `flat`, - ) - reach(`equal-duplicates`) - checkSetEquality(plainSetSpecs(different), plainSetSpecs(same), `flat`) - reach(`late-mismatch`) - checkSetEquality(plainSetSpecs(same.slice(1)), plainSetSpecs(same), `flat`) - reach(`cardinality`) - } -} - -declareOracle({ - model: [expectedSetEquality], - check: [checkSetEquality, checkSetFamily], - grammar: [setShapes, setWorkSizes, setMemberKinds], -}) - -/** - * An augmenting chain has a known bijection without solving production's - * matching problem. Gray-code columns differ in one coordinate. Each left - * word permits the two adjacent columns by using a plain record in that - * coordinate. Its other coordinates use the column's keyed classes. The final - * left word names column zero exactly. Matching every earlier word to its - * successor and the final word to zero is a complete assignment witness. - * - * A changed leaf in the final right column instead isolates that column: - * every left leaf is zero, so no left word can match it. These are sufficient - * positive and negative judgments for this grammar, not a general graph solver. - * Rotation varies Set insertion order. Flat matching depth must not become - * JavaScript call depth, even though arbitrary nested-value depth is unbounded - * by this instrument. Small generated chains and a finite 8,192-member chain - * share the same descriptor judgment and production comparison. - */ -type WordDescriptor = ReadonlyArray -export function setChainDescriptors( - bits: number, - broken: boolean, - rotate: boolean, -) { - const size = 2 ** bits - const column = (index: number): Array => { - const gray = index ^ (index >> 1) - return Array.from({ length: bits }, (_, coordinate) => ({ - kind: ((gray >> coordinate) & 1) === 0 ? `first` : `second`, - value: broken && index === size - 1 ? 1 : 0, - })) - } - const right = Array.from({ length: size }, (_, index) => column(index)) - const left = Array.from({ length: size }, (_, index) => { - const start = index === size - 1 ? 0 : index - const a = column(start).map((member) => ({ ...member, value: 0 })) - const b = column(index === size - 1 ? 0 : index + 1) - return a.map((member, coordinate): SetMemberSpec => ({ - ...member, - kind: member.kind === b[coordinate]!.kind ? member.kind : `plain`, - })) - }) - const witness = left.map((_, index) => (index + 1) % size) - if (rotate) { - right.push(right.shift()!) - for (const [index, slot] of witness.entries()) - witness[index] = (slot + size - 1) % size - } - return { left, right, witness } -} -function expectedChainEquality( - left: ReadonlyArray, - right: ReadonlyArray, - witness: ReadonlyArray, -): boolean { - const compatible = (a: WordDescriptor, b: WordDescriptor) => - a.length === b.length && - a.every( - (member, index) => - member.value === b[index]!.value && - (member.kind === `plain` || - b[index]!.kind === `plain` || - member.kind === b[index]!.kind), - ) - if ( - right.some((word) => - left.every((candidate) => !compatible(candidate, word)), - ) - ) - return false - if ( - left.length === right.length && - new Set(witness).size === right.length && - witness.every( - (slot, index) => - slot >= 0 && - slot < right.length && - compatible(left[index]!, right[slot]!), - ) - ) - return true - throw new Error( - `Chain grammar supplied neither a bijection nor an isolated-column witness`, - ) -} -export function checkSetChain(bits: number, broken: boolean, rotate: boolean) { - const { left, right, witness } = setChainDescriptors(bits, broken, rotate) - const expected = expectedChainEquality(left, right, witness) - const word = (specs: WordDescriptor) => [ - ...authoredSet(specs, `flat`, () => {}), - ] - expect(deepEquals(new Set(left.map(word)), new Set(right.map(word)))).toBe( - expected, - ) -} -declareOracle({ - model: [expectedChainEquality], - check: [checkSetChain], - grammar: [setChainDescriptors], -}) diff --git a/packages/db/tests/utils-oracle.property.test.ts b/packages/db/tests/utils-oracle.property.test.ts index a2d478ca29..322e197ef1 100644 --- a/packages/db/tests/utils-oracle.property.test.ts +++ b/packages/db/tests/utils-oracle.property.test.ts @@ -1,4 +1,4 @@ -import { afterAll, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' import { deepEquals } from '../src/utils' @@ -15,12 +15,6 @@ import { expectEqualityPair, expectRingLaw, } from './deep-equals-support' -import { createOracleReach } from './oracle-reach.js' -import { - checkSetChain, - checkSetFamily, - setShapes, -} from './set-equality-oracle.js' /** * The `deepEquals` API contract in `../src/utils.ts`, reinforced by the @@ -820,80 +814,4 @@ describe(`deepEquals property-based tests`, () => { }, ) }) - // Append this property so existing numeric replay selectors stay stable. - // Generated payload and order choices cross every class-pair/cycle shape; - // duplicate multiplicity and a late mismatch expose factorial work. - const setReach = createOracleReach(`utils Set equality`, [ - ...setShapes, - `equal-duplicates`, - `late-mismatch`, - `cardinality`, - ]) - let enteredSetProperty = false - afterAll(() => { - if (enteredSetProperty) setReach.assertReached() - if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) - console.info( - `utils Set equality reach`, - Object.fromEntries(setReach.counts()), - ) - }) - describe(`ambiguous Set membership`, () => { - utilsProperty([fc.integer({ min: -100, max: 100 }), fc.boolean()])( - `matches ambiguous object Sets with quadratic flat-member reads`, - (value, reverse) => { - enteredSetProperty = true - setReach.beginHistory() - checkSetFamily(value, reverse, (state) => setReach.reach(state)) - }, - ) - }) -}) - -// The chain family varies structural length, insertion order and a neighboring -// isolated-column violation. Both campaigns use the same descriptor judgment. -// The large finite witness checks flat matching depth without multiplying that -// expensive witness by the random campaign budget. -describe(`Set assignment paths`, () => { - const chainReach = createOracleReach(`Set augmenting chains`, [ - `assignment`, - `isolated`, - ]) - let entered = false - afterAll(() => { - if (entered) chainReach.assertReached() - if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) - console.info(`Set chain reach`, Object.fromEntries(chainReach.counts())) - }) - utilsProperty([fc.integer({ min: 2, max: 6 }), fc.boolean()])( - `preserves Set assignment across augmenting paths and isolated columns`, - (bits, rotate) => { - entered = true - chainReach.beginHistory() - for (const broken of [false, true]) { - checkSetChain(bits, broken, rotate) - chainReach.reach(broken ? `isolated` : `assignment`) - } - }, - ) - const scaleReach = createOracleReach( - `flat Set matching scale`, - [`positive`, `negative`], - { replaying: false }, - ) - let enteredScale = false - afterAll(() => { - if (enteredScale) scaleReach.assertReached() - if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) - console.info(`Set scale reach`, Object.fromEntries(scaleReach.counts())) - }) - it.each([false, true])( - `keeps flat Set matching off the call stack: isolated=%s`, - (broken) => { - enteredScale = true - scaleReach.beginHistory() - checkSetChain(13, broken, false) - scaleReach.reach(broken ? `negative` : `positive`) - }, - ) }) From 87000e96fea7cd1b3a672f5fd4ac12d9ddc6c4f4 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 30 Oct 2026 09:48:10 -0600 Subject: [PATCH 8/8] fix(db): preserve unordered Effect notifications with key tracking --- .changeset/fix-effect-left-join-startup.md | 2 +- docs/contributing/oracle-coverage.md | 25 +- .../oracle-reviews/effect-startup-delta.md | 212 +++++++++++- docs/guides/live-queries.md | 2 + packages/db/src/query/effect.ts | 36 +-- .../db/tests/query/effect-callback-oracle.ts | 305 ++++++++++++++++++ .../virtual-row-fields-effect-oracle.test.ts | 79 ++++- .../tests/effect-callback-oracle.test.tsx | 102 ++++++ 8 files changed, 733 insertions(+), 30 deletions(-) create mode 100644 packages/db/tests/query/effect-callback-oracle.ts create mode 100644 packages/react-db/tests/effect-callback-oracle.test.tsx diff --git a/.changeset/fix-effect-left-join-startup.md b/.changeset/fix-effect-left-join-startup.md index 60aa17d2ac..934954eeac 100644 --- a/.changeset/fix-effect-left-join-startup.md +++ b/.changeset/fix-effect-left-join-startup.md @@ -2,4 +2,4 @@ '@tanstack/db': patch --- -Classify Effect deltas from preceding result membership so LEFT-join startup and coalesced source writes emit the correct enter, update, or exit callback, including functional projections with callable or opaque values. Suppress updates when the projected result remains equal under change-event equality, and avoid that comparison when no update or batch handler observes it. +Classify Effect deltas from preceding result membership so LEFT-join startup and coalesced source writes emit the correct enter, update, or exit callback, including functional projections with callable or opaque values. Unordered Effects track only result keys and preserve their existing update notifications without comparing or retaining projected values. diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index c19a9d37d6..32a0bf9d44 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -1895,7 +1895,7 @@ The production matcher and the existing `utils.1`–`utils.40` campaigns and budgets remain unchanged. The synthetic duplicate-valued object-Set family exposes factorial assignment work, and an 8,192-member flat assignment chain exposes recursive stack depth. No application incident was established. -The [historical experiments](https://github.com/TanStack/db/blob/0b47ee825aaa0c36b199a3c42652cab8d9672ec9/docs/contributing/oracle-reviews/effect-startup-high-review.md) +The [versioned replay inputs](oracle-reviews/effect-startup-delta.md#deferred-set-replay-inputs) retain the descriptors, observations and limits. The primary owner remains `packages/db/tests/utils-oracle.property.test.ts`; resume optimization when the maintainer requests it or an application workload requires it. The new @@ -5789,7 +5789,7 @@ and these named paths. No real provider/framework, arbitrary scheduling, ordered repair, same-source overlap, callback-input sanitization or exact internal graph-run callback-count guarantee follows. -P09 is repaired in PR #2301 within this finite controlled-core boundary. +Historical initial P09 repair evidence for PR #2301: The exact owner and `test:oracles` entry were restored before production edits. On baseline `9b03e639f`, the fresh RED reproduced 64 startup failures with every reach cell at count 1. Repaired production passes all 216 owner tests and all @@ -5806,7 +5806,8 @@ Subsequent checker calibration keeps both callback channels coherent when injecting a bad update key. The `key-collision` and `key-change` controls now reject deletion of the interval-continuity judgment itself: the full checker mutant passes 214 controls/histories and fails those two controls. Repaired -production passes all 216 owner tests and the combined 619 targeted tests. +production passed all 216 owner tests and the combined 619 targeted tests at +that historical subject, including subsequently removed Set work. Native histories and all 5,512 reach cells are unchanged. See the follow-up's [calibration evidence](oracle-reviews/effect-startup-delta.md#key-interval-checker-calibration) for the new owner fingerprint and the limits of the continuity claim. @@ -5825,6 +5826,24 @@ identity, arbitrary nondeterministic projections, changing join keys, provider schedules or framework behavior. The earlier scalar and ordered-repair limits remain. See the [Effect membership evidence](oracle-reviews/effect-startup-delta.md#membership-and-coalesced-source-writes). +Current key-only repair: the metadata owner has **300 tests**, including 60 +callback-profile histories from `query/effect-callback-oracle.ts`. The actual +React driver `packages/react-db/tests/effect-callback-oracle.test.tsx` runs the +same 60 histories, model and comparison. Six payload representations cross +five callback profiles and `skipInitial`. Source-version updates, handler +replacement, exit, reentry, next use and disposal reach 480 cells on each path, +each at count 1. The original 5,512 cells and 120 membership cells remain. +On unchanged `2b3dd8c2d`, 56 core and 40 React histories fail. The repair passes +all **708 targeted tests**. The stale-value mutant survives the original 314 +tests and fails the extended histories at exit; gross-count membership fails +79 comparisons. Unordered Effects now retain keys only and never compare +projected values. Ordered repairs retain their preceding coherent values. +The maintainer explicitly chose preservation of unordered update notifications, +including equal projections, on 2026-10-30. The new histories cover eager core +sources and actual React hook commits; other providers/frameworks, arbitrary +schedules and ordered repair semantics retain their existing limits. See the +[key-only review evidence](oracle-reviews/effect-startup-delta.md#review-of-2b3dd8c2d-key-only-unordered-results). + ### RC bound schema mutation receivers (2026-10-28) Owner: `packages/db/tests/live-query-bound-options-oracle.ts`; shared driver: diff --git a/docs/contributing/oracle-reviews/effect-startup-delta.md b/docs/contributing/oracle-reviews/effect-startup-delta.md index 960de06889..a5c4b38245 100644 --- a/docs/contributing/oracle-reviews/effect-startup-delta.md +++ b/docs/contributing/oracle-reviews/effect-startup-delta.md @@ -1,5 +1,10 @@ # Effect startup delta follow-up +Current implementation: unordered Effects track keys only and preserve graph +update notifications. The [key-only review repair](#review-of-2b3dd8c2d-key-only-unordered-results) +supersedes the earlier retained-value/comparison designs below. Earlier counts, +fingerprints, commands and design claims apply only to their named subjects. + ## Baseline restoration This follow-up restores the accepted Effect metadata owner from @@ -93,11 +98,11 @@ The update's previous and current values were identical, with all right fields undefined. The independent before/after model rejected the extra event and the absence of its prior enter in both callback channels. -The repair applies the existing `deepEquals` update condition to unordered -Effects too. Ordered Effects already use this condition. It preserves genuine -changed-value updates without retaining every unordered result row. Equality -uses the existing change-event equality domain, not D2 hash identity. -This evidence checks scalar projections, not every value in either domain. +The initial repair applied the existing `deepEquals` update condition to +unordered Effects. That design used no retained unordered result rows and +checked only scalar projections. The later membership repair retained values, +and the final key-only repair removes both unordered value retention and +comparison. These historical designs are not current implementation claims. The earlier Effect examples lacked this exact startup delta comparison. The restored owner checks the complete emitted delta and active key intervals, @@ -365,3 +370,200 @@ The Effect source and owner fingerprints above still identify the verified implementation. Set production, its former `utils.1`–`utils.40` owner and its registry are byte-identical to the merge base. No full repository test or build ran beyond the separately authorized size comparison's normal package builds. + +## Review of 2b3dd8c2d: key-only unordered results + +The ten-item review targets `2b3dd8c2d0a0f98a858dc1d08182f6673c9a7836`. +It supplied no executed tests or builds. This evaluation is a self-review. +The maintainer explicitly chose: "Preserve existing notifications; use keys." +That decision supersedes the earlier general equal-projection suppression +policy recorded above. No oracle expectation changed to match production. +The new model ran RED before production changed. + +### Law, experiment and inference + +`EffectConfig`, `DeltaEvent` and the live-query guide promise complete delta +values at each callback cut. Handler subsets do not change an exit's current +value or an update's pre-batch value. React promises current callbacks without +remounting. The maintainer's decision preserves unordered graph notifications, +including equal projections, without adding value comparison or retention. + +The primary owner adds a shared companion, `effect-callback-oracle.ts`, and an +actual React receiving driver. Sixty finite histories on each path cross: + +- Scalar, keyless opaque class, keyed private-field class, prototype method over + non-enumerable state, object Set and equal scalar projection. +- All callbacks, enter only, exit only, late update and late batch callbacks. +- Initial delivery and `skipInitial`. + +Authored scalar source versions determine expected public readable payloads. +Classes and Sets are constructed only in the actual functional projection, +so source-Collection comparison cannot mask or create the reported output cost. +The model never imports production equality or its delta classifier. Both paths +compare initial delivery, an unobserved update, callback replacement, another +update, exit, reentry, next use and disposal. Source updates while absent are +excluded. Removing any profile loses its callback premise; removing any payload +loses its specific comparison boundary. All combinations run, and every one of +480 declared cells on each receiving path reaches once. Original 200/24 +histories and their 5,512/120 cells remain unchanged. + +The recorder reads actual public payloads, including private-state accessors, +and retains event multiplicity and previous values. Synthesized Set getters +count reads outside the recorder. The restored unordered path traverses no +payload leaves, for any callback profile. This bounds added traversal in these +histories, not Set runtime or arbitrary graphs. The original metadata histories +still check opaque-key intervals and both channels independently. + +| Experiment | Result | +| --- | --- | +| Expanded tests on unchanged reviewed production | 264 pass, 96 intended failures: 56 core and 40 React. Original 240 tests pass. | +| Repaired core owner and React receiver | 360 pass, zero failures/skips. | +| Final eight targeted suites | 708 pass, zero failures/skips. | +| Reviewed classifier with refresh skipped for unobserved updates, older tests only | 314 pass; the 120 new histories are deliberately unselected for this diagnostic. The mutant survives. | +| Same stale-value mutant, expanded tests | 338 pass, 96 failures. An exit reports version 0 instead of current version 2. | +| Gross-count prior-membership mutant (`wasPresent = deletes > 0`) | 355 pass, 79 intended callback/membership failures. | + +All ordinary runs and full mutant runs preserve per-path reach. The diagnostic +old-test selection is not a normal GREEN claim. Initial React module resolution +failed before collection; explicit aliases to the already-installed React 19.2.4 +fixed that setup before the valid pre-production RED. No setup failure counts +as a product or mutant result. + +The final repair retains only keys for unordered Effects. The first deletion +in a batch supplies its previous/exiting value. Ordered Effects still retain +coherent values across held graph runs and use their existing comparison. +`observesUpdates` and its creation-time snapshot are removed. Dynamic callback +lookup remains live. The redundant value fallbacks are removed; `deleteValue` +is now necessary for unordered delivery rather than dead state. + +A deterministic cardinality probe at 8 and 64 rows confirms that reviewed +unordered Effects added one Map entry per result, holding the same row objects +as callbacks. It did not clone the rows. Disposal cleared that Map. The repaired +path retains no output values in a Map. No total-memory multiplier was measured. + +### Lossless disposition + +| ID | Technical verdict | PR action and durable value | +| --- | --- | --- | +| R2-01 | Extra per-row Map retention confirmed. "Full copy" and roughly double total memory are unsupported: rows are shared references. | Fixed now with key-only unordered tracking for every handler profile, rather than conditional retained values. | +| R2-02 | Confirmed new comparison path. Scalar sources that synthesize object Sets isolate it from source comparison. | Fixed now: unordered Effects never call the comparator. The removed Set rewrite stays removed. | +| R2-03 | Confirmed: React always supplies wrappers. The claimed unordered comparison bypass did not apply there. | Fixed now through the core repair and real React receiving witness. Existing ordered comparison is unchanged. | +| R2-04 | Confirmed test gap and mutant survival. | Fixed now with callback profiles, replacement, current exit values and a demonstrated stale-value mutant rejection. The obsolete optimization branch is removed. | +| R2-05 | Partially confirmed. Keyless private instances already compare by identity and notify. Keyed instances with constant enumerable fields can hide changed private/non-enumerable state and retain stale values. | Fixed now on the introduced unordered path. The grammar keeps both failing and non-failing representations. No new general class-equality contract is imposed on other callers. | +| R2-06 | Confirmed: the initial observer snapshot prevents later core update/batch installation. | Fixed now by removing the snapshot; actual core config mutation and React replacement both run through the shared comparison. | +| R2-07 | Dead fallbacks are confirmed at the reviewed Map-only subject. Deleting the accumulator field after the key-only repair would instead lose previous/exiting values. | Fixed now by removing redundant fallbacks and restoring the field's required role. No field removal is claimed. | +| R2-08 | Duplicate retention remedy of R2-01. "Wrong layer" is a design opinion, not a demonstrated contract violation. Prior membership is needed beyond a raw gross-count classification. | Duplicate of R2-01; keep this separate ID and its key-only suggestion. The maintainer selected the narrower design in the Effect boundary. | +| R2-09 | Confirmed ambiguous/stale evidence. | Fixed now: historical subjects are explicit, and current owner/receiver counts and implementation are separate. | +| R2-10 | Local-only traces are a durability limit. Squash does not automatically remove GitHub commit blobs: both this PR's earlier blob and merged #2299's pre-squash blob remain accessible. Future retention is not guaranteed by that observation. | Fixed now by retaining essential replay inputs below and finite histories in current versioned sources. Historical blob links are supplementary, and local logs are convenience artifacts. | + +Accounting: **10 = 9 fixed-now + 1 duplicate**. Each technical qualification +above survives its action verdict. No new item is deferred. Previously removed +Set engineering work retains the maintainer's explicit scope decision and its +primary equality owner; resumption requires a maintainer request or an actual +workload. Removing the new unordered comparator caller resolves the new +interaction without reopening that optimization project. + +Reviewer assessment: **lean hire for static defect discovery, with verification +required before approval**. Callback profiles, live replacement and React +wrappers expose important missing cases. The memory multiplier, blanket private +class claim, unconditional link-loss claim and field-deletion suggestion needed +qualification. The proposed membership-only direction was useful. Severity and +closure require executed evidence. + +### Oracle review and remaining limits + +This is bounded enforcement of unordered membership, notification compatibility +and callback-visible values for the declared eager source histories and actual +React commits. No known violation remains within these cells. It does not prove +arbitrary scheduling, provider delivery, changing join multiplicity, other +frameworks, ordered opaque-value semantics or exact rich-instance identity. +Other comparator callers retain their previously documented domains and limits. + +| Requirement | Current evidence | +| --- | --- | +| ORC-001 | Public delta contracts and explicit maintainer notification decision above. | +| ORC-002 | Authored source-version model; no production comparator/classifier import. | +| ORC-003 | Companion explains contract, model, grammar, drivers and comparisons beside code. | +| ORC-004 | Full finite crossing reconstructs the reports, states axis ablations and bounds, and excludes updates of absent sources. | +| ORC-005 | Core callbacks and actual React hook; current readable values checked after each host/act cut. Scalar source facts arrive before projection. | +| ORC-006 | Stale-value and gross-count mutants fail intended exit/value/membership comparisons. The former survives the old tests. | +| ORC-007 | Not triggered: bounded finite histories, no random property or new run budget. | +| ORC-008 | No stateful reference-state expansion; one authored prior source version suffices for public delta values. | +| ORC-009 | Source version abstracts readable selected payload, with the mapping stated beside model and projection. | +| ORC-010 | Per-cut mismatches accumulate; shared cleanup preserves primary failure and separate cleanup diagnostics. Disposal has its own cut. | +| ORC-011 | No unresolved shared semantic fault is identified for this version-to-readable-payload relation. Other formulations remain outside the bounded claim. | +| ORC-012 | Reviewed head, exact fingerprints, experiments, all IDs and every rubric requirement are versioned here. | +| ORC-013 | Equal versus changed output, keyless versus keyed opaque state, callback presence/replacement and exit after unobserved updates distinguish neighboring wrong designs. | +| ORC-014 | Actual React renderHook/act supplies the callback-replacement premise; each path has separate reach. No other framework/provider claim. | +| ORC-015 | 480 cells per new path, each 1; original 5,512 and added membership 120 remain each 1. | +| ORC-016 | Finite histories use shared model/driver/comparison without per-case expected answers. Scratch retention probes are separate diagnostics. | + +The final run selects utils oracle, utils, proxy revert, Effect, Effect disposal, +Effect metadata, React Effect callbacks and ordinary React Effect tests. It uses +jsdom, existing test setup, source aliases for DB/IVM, installed React 19.2.4, +Vitest 3.2.4, no coverage, and at most two workers. Standard selectors are: + +```sh +pnpm --filter @tanstack/db exec vitest run --coverage.enabled=false --maxWorkers=2 \ + tests/utils-oracle.property.test.ts tests/utils.test.ts \ + tests/proxy-revert-oracle.property.test.ts tests/effect.test.ts \ + tests/effect-disposal-oracle.test.ts tests/query/virtual-row-fields-effect-oracle.test.ts +pnpm --filter @tanstack/react-db exec vitest run --coverage.enabled=false --maxWorkers=2 \ + tests/effect-callback-oracle.test.tsx tests/useLiveQueryEffect.test.tsx +``` + +Local execution uses the same selectors in the source-alias config +`/private/tmp/effect-startup-delta/review-2b3dd8c/final.config.mjs`. Reports, +commands, original/mutant snapshots and the append-only raw review ledger remain +in that directory. These local files are supplementary to the versioned +histories, recipe and comparison. Targeted TypeScript and ESLint pass. +The authorized `pnpm size:compare` reports typical app 96,732→96,732 bytes gzip, +Effects app 99,303→99,309 (+6), and DB bundle 109,667→109,668 (+1), against merge +base `bea84cd45`. Other app scenarios are unchanged. No full test/build ran +beyond the size script's normal package builds. + +Exact subject: reviewed parent plus the following final source fingerprints. +Documentation-only changes do not alter these executable subjects. + +```text +354bbbcf9683c91047c61da1ea2323ed89623628eb6059dacfbb79bd56b85ce4 packages/db/src/query/effect.ts +47e862b092e643309bd804acab7c61c036ed7a345c873ca480f63b807aa4f24d packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts +ca9e1336e7ceeb243a8e32a49ad2c3c881deaa38334540e85f848122cbaf2d38 packages/db/tests/query/effect-callback-oracle.ts +9ed573ae14bf9bd87058c9136ef1de57f33422da235f6253a9c97f90599cf98e packages/react-db/tests/effect-callback-oracle.test.tsx +``` + +### Deferred Set replay inputs + +These inputs preserve the earlier synthetic findings without reinstating the +Set rewrite, test campaigns or work budgets. No application incident or new +latency guarantee follows. The existing `utils-oracle.property.test.ts` owner +remains responsible for change-event equality; the maintainer controls whether +to resume the excluded engineering work. + +The factorial-work witness uses distinct object members. For size `n`, compare +`new Set(Array.from({length:n}, () => ({value:0})))` with a Set of `n-1` such +objects followed by `{value:1}`. The equality judgment is false. Instrument an +enumerable `value` getter to count reads. The earlier direct `n=4` experiment +observed 128 reads, against its proposed bound of 32. The eight-member Effect +receiver observed 219,200 reads against 1,296 while still returning the correct +equality result. Those engineering bounds are historical and retired. + +The stack witness uses `n=2**13` words of 13 coordinates. Let `g(j)=j^(j>>1)`. +For right word `j`, coordinate `k` is a distinct instance of class First or +Second according to bit `k` of `g(j)`, with one enumerable `value:0` field. +Two different classes differ; a plain `{value:0}` can match either class. +Left word `i (i+1)%n`, so equality is true. To create the false neighbor, change every +coordinate value in the last right word to 1, leaving an isolated unmatched +word. The earlier recursive matcher overflowed on both witnesses after setup. +This is a runtime failure, not an equality-assertion rejection. Smaller generated +versions used 2–6 bits and optional one-place right rotation. A recorded greedy +mutant replay used `utils.42`, seed `-75265719`, shrink path `1`; that retired +selector alone does not run on the current registry. The structural recipe +above supplies the inputs independently of the removed generator. + +Historical reports at `0b47ee825aaa0c36b199a3c42652cab8d9672ec9` retain broader +experiments. Their links are supplementary; the current source tree now holds +these essential distinguishing inputs, observations and limits. diff --git a/docs/guides/live-queries.md b/docs/guides/live-queries.md index ba264a1846..65b63485b9 100644 --- a/docs/guides/live-queries.md +++ b/docs/guides/live-queries.md @@ -2450,6 +2450,8 @@ interface DeltaEvent { | `exit` | Row left the query result | The exiting row | — | | `update` | Row changed but stayed in the result | The new row | The row before the change | +Unordered Effects classify graph deltas using result membership. They keep result keys without retaining projected rows or comparing projected values. A graph update can therefore notify handlers even when the projected fields compare equal. Ordered Effects retain the last coherent result for repair across graph runs and suppress equal-value updates. Callback presence does not change the delta values. + ### Named Callbacks Use the callback that matches the query-result transition you care about: diff --git a/packages/db/src/query/effect.ts b/packages/db/src/query/effect.ts index bae626604a..4355c7a753 100644 --- a/packages/db/src/query/effect.ts +++ b/packages/db/src/query/effect.ts @@ -337,7 +337,6 @@ export function createEffect< effect, query: config.query, skipInitial: config.skipInitial ?? false, - observesUpdates: Boolean(config.onUpdate || config.onBatch), onBatchProcessed, onHealthChange: config.onHealthChange, onSourceError: (error: Error) => { @@ -402,7 +401,6 @@ interface EffectPipelineRunnerConfig< > { query: EffectQueryInput skipInitial: boolean - observesUpdates: boolean onBatchProcessed: (events: Array>) => void /** Called when a source collection enters error or cleaned-up state */ onSourceError: (error: Error) => void @@ -485,13 +483,12 @@ class EffectPipelineRunner { // Output accumulator private pendingChanges: Map> = new Map() - // The preceding coherent result, including the skipped initial baseline. - // Multiplicity and value equality alone cannot determine prior membership. - private readonly publishedRows = new Map() + // Unordered deltas need prior membership, not retained values. Ordered + // repairs additionally need the preceding coherent values across graph runs. + private publishedRows: Set | Map = new Set() // skipInitial state private readonly skipInitial: boolean - private readonly observesUpdates: boolean private initialLoadComplete = false // The Effect's initial gate: latched when every // initial load has settled, with or without skipInitial. After it, @@ -517,7 +514,6 @@ class EffectPipelineRunner { constructor(config: EffectPipelineRunnerConfig) { this.skipInitial = config.skipInitial - this.observesUpdates = config.observesUpdates this.onBatchProcessed = config.onBatchProcessed this.onSourceError = config.onSourceError this.onHealthChange = config.onHealthChange @@ -746,6 +742,7 @@ class EffectPipelineRunner { // For ordered aliases with an index, trigger the initial limited snapshot. // This loads only the top N rows rather than the entire collection. if (orderByInfo) { + this.publishedRows = new Map() const loader = new OrderedSourceLoader( orderByInfo, subscription, @@ -1071,13 +1068,13 @@ class EffectPipelineRunner { key as TKey, changes, this.publishedRows, - shouldPublish && this.observesUpdates, ) if (event) { if (event.type === `exit`) this.publishedRows.delete(key) - else this.publishedRows.set(key, event.value) - if (shouldPublish && (event.type !== `update` || this.observesUpdates)) - events.push(event) + else if (this.publishedRows instanceof Map) + this.publishedRows.set(key, event.value) + else this.publishedRows.add(key) + if (shouldPublish) events.push(event) } } @@ -1326,12 +1323,12 @@ function accumulateEffectChanges( function classifyDelta( key: TKey, changes: EffectChanges, - publishedRows: ReadonlyMap, - observesUpdates: boolean, + publishedRows: ReadonlySet | ReadonlyMap, ): DeltaEvent | undefined { const { inserts, deletes, insertValue, deleteValue } = changes const wasPresent = publishedRows.has(key) - const previousValue = publishedRows.get(key) ?? deleteValue + const previousValue = + publishedRows instanceof Map ? publishedRows.get(key) : deleteValue const isPresent = Number(wasPresent) + inserts - deletes > 0 if (!wasPresent && isPresent) { @@ -1342,17 +1339,17 @@ function classifyDelta( if (wasPresent && !isPresent) { // Row exited the query result — value is the exiting value, // previousValue is omitted (it would be identical to value) - return { type: `exit`, key, value: previousValue ?? deleteValue! } + return { type: `exit`, key, value: previousValue! } } if ( wasPresent && isPresent && insertValue !== undefined && - (!observesUpdates || !deepEquals(previousValue, insertValue)) + (publishedRows instanceof Set || !deepEquals(previousValue, insertValue)) ) { - // Presence is already established. Compare values only for a consumer that - // observes updates; otherwise refresh the retained value for a later exit. + // Unordered Effects preserve all graph updates without comparing values. + // Ordered repairs compare with the preceding coherent publication. return { type: `update`, key, @@ -1361,8 +1358,7 @@ function classifyDelta( } } - // The row was absent both before and after the held delta, or its final - // value is equal to the last callback-visible value. + // The row was absent at both cuts, or an ordered repair kept an equal value. return undefined } diff --git a/packages/db/tests/query/effect-callback-oracle.ts b/packages/db/tests/query/effect-callback-oracle.ts new file mode 100644 index 0000000000..333a98959e --- /dev/null +++ b/packages/db/tests/query/effect-callback-oracle.ts @@ -0,0 +1,305 @@ +/** + * Callback profiles must not change the query's delta values. EffectConfig and + * the Effect guide promise the current exiting value and the pre-batch update + * value. React additionally promises the latest callbacks without remounting. + * This extends the metadata owner's observation to handler subsets and values + * whose readable state is absent from enumerable fields. + * + * An authored scalar source version independently predicts each readable + * payload. The real query projection creates classes/Sets. The driver invokes + * production. The finite grammar crosses payload representation, callback profile and initial + * suppression. Every history updates before and after callback replacement, + * then exits, reenters and updates again. An update of an absent source is not + * legal. These eager single-source histories do not cover ordered repairs or + * arbitrary provider schedules. Both receiving drivers use this same judgment. + * The maintainer's 2026-10-30 decision preserves unordered notifications even + * for equal projections and permits key-only membership tracking. A synthesized + * Set counts leaf reads outside observation capture: these unordered Effects + * must not add traversal of projected values. This is not a Set latency law. + */ +import { expect } from 'vitest' +import { declareOracle } from '../oracle-declaration.js' +import { createOracleReach } from '../oracle-reach.js' +import { checkWithCleanup } from './virtual-row-fields-support.js' +import type { DeltaEvent, EffectConfig } from '../../src/query/effect.js' + +export type CallbackRow = { id: number; version: number } +export type CallbackOutput = { id: number; payload: unknown } +type Representation = + `scalar` | `opaque` | `private` | `prototype` | `set` | `equal` +export const callbackHistories = ( + [`scalar`, `opaque`, `private`, `prototype`, `set`, `equal`] as const +).flatMap((representation) => + ([`all`, `enter`, `exit`, `late-update`, `late-batch`] as const).flatMap( + (profile) => + [false, true].map((skipInitial) => ({ + representation, + profile, + skipInitial, + })), + ), +) +export type CallbackHistory = (typeof callbackHistories)[number] +export type CallbackHandlers = Pick< + EffectConfig, + `onEnter` | `onUpdate` | `onExit` | `onBatch` +> +export interface CallbackDriver { + mount: (options: { + skipInitial: boolean + project: (row: CallbackRow) => CallbackOutput + handlers: CallbackHandlers + }) => Promise<{ + write: ( + type: `insert` | `update` | `delete`, + version: number, + ) => Promise + replaceHandlers: (handlers: CallbackHandlers) => Promise + dispose: () => Promise + cleanup: () => Promise + }> +} +type Observation = { + channel: string + type: string + value: string + previous?: string +} +const cuts = [ + `initial`, + `unobserved`, + `replacement`, + `update`, + `exit`, + `reentry`, + `next-use`, + `disposed`, +] +export function callbackHistoryId(h: CallbackHistory) { + return `${h.representation}/${h.profile}/${h.skipInitial}` +} +export function callbackReach(path: string) { + return createOracleReach( + `Effect callback delivery ${path}`, + callbackHistories.flatMap((h) => + cuts.map((cut) => `${callbackHistoryId(h)}/${cut}`), + ), + { replaying: false }, + ) +} + +class PrivatePayload { + #version: number + constructor(version: number) { + this.#version = version + } + read() { + return this.#version + } +} +class PrototypePayload { + currency = `USD` + constructor(version: number) { + Object.defineProperty(this, `version`, { value: version }) + } + declare version: number + read(): number { + return this.version + } +} +class KeyedPrivatePayload extends PrivatePayload { + currency = `USD` +} + +// Full source-version recomputation defines readable values, not deepEquals or +// the production membership classifier. Sets retain three authored members. +function expectedPayload( + representation: Representation, + version: number, +): string { + return representation === `set` + ? `0,0,${version}` + : representation === `equal` + ? `0` + : String(version) +} +function readPayload(payload: unknown): string { + if (payload instanceof PrivatePayload || payload instanceof PrototypePayload) + return String(payload.read()) + if (payload instanceof Set) + return [...payload] + .map((v: { value: number }) => v.value) + .sort() + .join(`,`) + return String(payload) +} +function expectedDelivery( + representation: Representation, + before: number | undefined, + after: number | undefined, + channels: ReadonlySet, + suppressed: boolean, +): Array { + if (suppressed || before === after) return [] + const type = + before === undefined ? `enter` : after === undefined ? `exit` : `update` + const event = { + type, + value: expectedPayload(representation, after ?? before!), + ...(type === `update` + ? { previous: expectedPayload(representation, before!) } + : {}), + } + return [...channels] + .filter((c) => c === `batch` || c === type) + .map((channel) => ({ channel, ...event })) +} +function checkDelivery( + actual: Array, + expected: Array, +) { + const order = (a: Observation, b: Observation) => + a.channel.localeCompare(b.channel) + expect([...actual].sort(order)).toStrictEqual([...expected].sort(order)) +} +declareOracle({ + model: [expectedPayload, expectedDelivery], + grammar: [callbackHistories], + check: [checkDelivery], +}) + +export async function runCallbackHistory( + history: CallbackHistory, + driver: CallbackDriver, + reach: ReturnType, +) { + reach.beginHistory() + const observed: Array = [] + let recording = false + let comparisonReads = 0 + let channels = new Set( + history.profile === `all` + ? [`enter`, `update`, `exit`, `batch`] + : history.profile.startsWith(`late`) + ? [`exit`] + : [history.profile], + ) + const record = (channel: string, event: DeltaEvent) => { + recording = true + try { + observed.push({ + channel, + type: event.type, + value: readPayload(event.value.payload), + ...(event.type === `update` + ? { previous: readPayload(event.previousValue.payload) } + : {}), + }) + } finally { + recording = false + } + } + const handlers = (): CallbackHandlers => ({ + onEnter: channels.has(`enter`) ? (e) => record(`enter`, e) : undefined, + onUpdate: channels.has(`update`) ? (e) => record(`update`, e) : undefined, + onExit: channels.has(`exit`) ? (e) => record(`exit`, e) : undefined, + onBatch: channels.has(`batch`) + ? (events) => { + for (const e of events) record(`batch`, e) + } + : undefined, + }) + const project = ({ id, version }: CallbackRow): CallbackOutput => { + let payload: unknown = version + if (history.representation === `opaque`) + payload = new PrivatePayload(version) + if (history.representation === `private`) + payload = new KeyedPrivatePayload(version) + if (history.representation === `equal`) payload = 0 + if (history.representation === `prototype`) { + payload = new PrototypePayload(version) + } + if (history.representation === `set`) + payload = new Set( + [0, 0, version].map((value) => ({ + get value() { + if (!recording) comparisonReads++ + return value + }, + })), + ) + return { id, payload } + } + const failures: Array = [] + let handle: Awaited> | undefined + let before: number | undefined + const cut = (name: string, after: number | undefined, suppressed = false) => { + try { + checkDelivery( + observed, + expectedDelivery( + history.representation, + before, + after, + channels, + suppressed, + ), + ) + } catch (error) { + failures.push(new Error(`callback cut ${name}`, { cause: error })) + } + observed.length = 0 + before = after + reach.reach(`${callbackHistoryId(history)}/${name}`) + } + await checkWithCleanup(async () => { + handle = await driver.mount({ + skipInitial: history.skipInitial, + project, + handlers: handlers(), + }) + cut(`initial`, 0, history.skipInitial) + await handle.write(`update`, 1) + cut(`unobserved`, 1) + if (history.profile === `late-update`) channels.add(`update`) + if (history.profile === `late-batch`) channels.add(`batch`) + await handle.replaceHandlers(handlers()) + cut(`replacement`, 1) + await handle.write(`update`, 2) + cut(`update`, 2) + // Installing exit after unobserved updates distinguishes current values + // from an initial value retained only when update handlers were present. + channels = new Set([`exit`]) + await handle.replaceHandlers(handlers()) + await handle.write(`delete`, 2) + cut(`exit`, undefined) + channels = new Set([`enter`, `update`, `exit`, `batch`]) + await handle.replaceHandlers(handlers()) + await handle.write(`insert`, 3) + cut(`reentry`, 3) + await handle.write(`update`, 4) + cut(`next-use`, 4) + await handle.dispose() + await handle.write(`update`, 5) + cut(`disposed`, 4) + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `Effect comparison reads`, + callbackHistoryId(history), + comparisonReads, + ) + try { + expect(comparisonReads, `unordered output leaf traversal`).toBe(0) + } catch (error) { + failures.push(error) + } + if (failures.length) + throw new AggregateError(failures, `Effect callback delivery failed`, { + cause: failures[0], + }) + }, [ + async () => { + await handle?.cleanup() + }, + ]) +} diff --git a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts index 30db585a49..6456720898 100644 --- a/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts +++ b/packages/db/tests/query/virtual-row-fields-effect-oracle.test.ts @@ -42,6 +42,11 @@ * Exact typed source identity is checked through ordinary observedKey instead. * The four unclaimed Effect-root virtual names are excluded from the ordinary * field projection, not interpreted as an output-row attribution policy. + * + * The callback-profile companion below also checks equal-projection graph + * updates under the maintainer's preservation decision. The original metadata + * histories change selected source facts and retain their array-difference + * judgment; they do not specify every unordered graph notification. */ import { isDeepStrictEqual } from 'node:util' import { afterAll, expect, test } from 'vitest' @@ -57,8 +62,14 @@ import { checkWithCleanup, virtualFieldNames, } from './virtual-row-fields-support.js' -import type { DeltaEvent } from '../../src/query/effect.js' +import { + callbackHistories, + callbackReach, + runCallbackHistory, +} from './effect-callback-oracle.js' +import type { DeltaEvent, EffectConfig } from '../../src/query/effect.js' import type { Transaction } from '../../src/transactions.js' +import type { CallbackOutput, CallbackRow } from './effect-callback-oracle.js' type Key = string | number type Side = `left` | `right` @@ -278,6 +289,72 @@ const effectMetadataHistories: Array = ( ), ), ) + +// Handler profiles and readable opaque values share their model/comparison +// with the real React receiver. This driver supplies only the core boundary. +const deliveryReach = callbackReach(`core`) +let ranDelivery = false +afterAll(() => { + if (ranDelivery) deliveryReach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `Effect core callback reach`, + Object.fromEntries(deliveryReach.counts()), + ) +}) +test.each(callbackHistories)( + `delivers current values across callback profiles: %j`, + async (history) => { + ranDelivery = true + await runCallbackHistory( + history, + { + mount: async ({ skipInitial, project, handlers }) => { + const source = createCollection( + mockSyncCollectionOptions({ + id: `callback-core`, + getKey: (row) => row.id, + initialData: [{ id: 1, version: 0 }], + }), + ) + await source.preload() + const config: EffectConfig = { + query: () => + new Query() + .from({ row: source }) + .fn.select(({ row }) => project(row)), + skipInitial, + ...handlers, + } + const effect = createEffect(config) + await flushPromises() + return { + write: async (type, version) => { + source.utils.begin() + source.utils.write({ type, value: { id: 1, version } }) + source.utils.commit() + await flushPromises() + }, + replaceHandlers: async (replacement) => { + Object.assign(config, replacement) + await flushPromises() + }, + dispose: () => effect.dispose(), + cleanup: () => + checkWithCleanup(async () => { + await effect.dispose() + }, [ + async () => { + await source.cleanup() + }, + ]), + } + }, + }, + deliveryReach, + ) + }, +) function pathOf(history: Pick) { return `${history.kind}${history.kind === `union` ? (history.reverse ? `-right-first` : `-left-first`) : ``}/${history.projection}` } diff --git a/packages/react-db/tests/effect-callback-oracle.test.tsx b/packages/react-db/tests/effect-callback-oracle.test.tsx new file mode 100644 index 0000000000..8608fb34be --- /dev/null +++ b/packages/react-db/tests/effect-callback-oracle.test.tsx @@ -0,0 +1,102 @@ +/** + * React receiving driver for the core Effect metadata owner's callback law. + * The shared oracle authors source versions, callback changes and comparisons. + * Real renderHook/act commits supply the hook's callback-replacement premise. + * Observation cuts follow act and one host turn. This covers eager sources, + * not arbitrary host scheduling or other framework bindings. + */ +import { afterAll, test } from 'vitest' +import { act, renderHook } from '@testing-library/react' +import { Query, createCollection } from '@tanstack/db' +import { useLiveQueryEffect } from '../src/useLiveQueryEffect' +import { + callbackHistories, + callbackReach, + runCallbackHistory, +} from '../../db/tests/query/effect-callback-oracle' +import { checkWithCleanup } from '../../db/tests/query/virtual-row-fields-support' +import { flushPromises, mockSyncCollectionOptions } from '../../db/tests/utils' +import type { + CallbackOutput, + CallbackRow, +} from '../../db/tests/query/effect-callback-oracle' +import type { EffectConfig } from '@tanstack/db' + +const reach = callbackReach(`React`) +let ran = false +afterAll(() => { + if (ran) reach.assertReached() + if (process.env.TANSTACK_DB_ORACLE_STATISTICS === `1`) + console.info( + `Effect React callback reach`, + Object.fromEntries(reach.counts()), + ) +}) +test.each(callbackHistories)( + `receives current Effect values across callback profiles: %j`, + async (history) => { + ran = true + await runCallbackHistory( + history, + { + mount: async ({ skipInitial, project, handlers }) => { + const source = createCollection( + mockSyncCollectionOptions({ + id: `callback-react`, + getKey: (row) => row.id, + initialData: [{ id: 1, version: 0 }], + }), + ) + await source.preload() + const query = new Query() + .from({ row: source }) + .fn.select(({ row }) => project(row)) + const mounted = renderHook( + ({ config }: { config: EffectConfig }) => + useLiveQueryEffect(config), + { initialProps: { config: { query, skipInitial, ...handlers } } }, + ) + await act(async () => { + await flushPromises() + }) + return { + write: async (type, version) => { + await act(async () => { + source.utils.begin() + source.utils.write({ type, value: { id: 1, version } }) + source.utils.commit() + await flushPromises() + }) + }, + replaceHandlers: async (replacement) => { + mounted.rerender({ + config: { query, skipInitial, ...replacement }, + }) + await act(async () => { + await flushPromises() + }) + }, + dispose: async () => { + mounted.unmount() + await act(async () => { + await flushPromises() + }) + }, + cleanup: () => + checkWithCleanup(async () => { + mounted.unmount() + await act(async () => { + await flushPromises() + }) + }, [ + async () => { + await source.cleanup() + }, + ]), + } + }, + }, + reach, + ) + }, +)