Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e936657
fix(db): throw when a retired bucket keeps rows the graph sent
Oct 9, 2026
7ebb30e
fix(db): decide facade writes from the adapter's own row record
Oct 9, 2026
a11af7a
fix(db): restore a held facade row as an update
Oct 9, 2026
3325a79
test(db): generate facade cleanup and shared optimistic ids
Oct 9, 2026
b8e123e
test(db): observe facade readiness and the change-message protocol
Oct 9, 2026
14eb98d
refactor(db): read facade rows from the accepted sync projection
Oct 10, 2026
ec0e102
fix(db): restore facades into an open sync transaction and drop the d…
Oct 10, 2026
50a8147
docs(db): describe facade decisions by projected synced rows
Oct 10, 2026
ee130fc
Merge origin/main into fix-facade-retire-leftover-rows
Oct 10, 2026
5192bb4
fix(db): make a cleaned-up child collection final
Oct 10, 2026
ea49a03
fix(db): keep a facade row's key in an optimistic update's copy
Oct 10, 2026
937f656
fix(db): write nothing for a facade key that nets to no change while …
Oct 11, 2026
5d2549a
fix(db): refuse a holder's cleanup of a live query's child collection
Oct 11, 2026
43ad7aa
Merge origin/main into fix-facade-retire-leftover-rows
Oct 11, 2026
eba2934
fix(db): leave a facade key alone in restore when it netted to no change
Oct 11, 2026
a34622f
Merge origin/main into fix-facade-retire-leftover-rows
Oct 11, 2026
507efdc
test(db): give each generated facade update a fresh value
Oct 11, 2026
6def9f5
test(db): live query cleanup inside root publication suppresses the f…
Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/fix-facade-retire-leftover-rows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/db': patch
---

A live query with included child collections now throws when the query graph retires a child collection that still has rows the graph never removed. The graph always removes a child collection's rows before it retires it, so this is an internal invariant violation. Before, the rows were removed silently, which hid the fault. The failed flush restores the child collections, as for any failed flush.

Calling `cleanup()` on a child collection of a live query now returns a rejected promise. The live query owns its child collections, and several parent rows can share one. Before, a cleanup stopped the child collection for every parent that shared it, so they never saw its rows again. Clean up the live query instead. Ordinary Collections are not affected.

An optimistic update of a row in a child collection no longer throws "Bucket facade row has no public key". Each child row now carries its key in `$key`, so the copy that an update makes keeps it.

Also fix a lost child row behind a persisting transaction. When a user transaction on a child collection was persisting, an update to a child row that an earlier flush had written was applied as a delete, so the row disappeared when the transaction settled. A rollback in that state could also delete such a row. The adapter now decides each write from the child collection's synced rows, including writes that a persisting transaction holds, not from the rows the child collection shows.
2 changes: 1 addition & 1 deletion docs/contributing/oracle-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ that test identifiers must copy production's private data structures.
| ---------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Ordered relations and BTree | Complete | The signed top-K relation, BTree/Map refinement model, and DBSP incrementalization laws are literate. |
| D2 operator work | `packages/db-ivm/tests/d2-work-oracle.test.ts` | A finalized 65-branch graph receives one direct input at the first, middle, or last branch, then a second input on a different branch. The oracle compares exact forwarded messages and operator invocations after each `run()`: branches without queued input perform no operator work. A connected reversed-registration pair must drain in the same run, and an empty unfinalized graph rejects. The old unconditional loop fails with 64 idle calls per turn. This bounded work check does not establish elapsed latency, fused operators, arbitrary graph topologies, or provider-level live-query work. |
| Includes and publication | Complete, [facade rollback](https://github.com/TanStack/db/blob/main/packages/db/tests/query/bucket-facade-rollback-oracle.property.test.ts) ([review](oracle-reviews/perf-lazy-bucket-rollback-copy.md)) | The central recomputation model plus cross-formulation, temporal demand, layered publication, Collection facade lifecycle and space bounds, route-context transport, functional projection, query-shape, optimistic, and source-work owners are literate. They keep the architecture document as their contract source. A failed facade flush restores every facade it wrote, disposes the facades it created, publishes no event or layout revision, and reads the stored rows of no other facade; nested facades across two edges have a pinned witness. The grammar adds optimistic facade rows, and retirement retracts every key a facade still shows. Every change pending at a failed root commit, child rows included, stays pending, and the next flush publishes it once. Open: the flush's edge and bucket map copy is unmeasured bookkeeping; disposal between a flush and its rollback has pinned witnesses only; held facade commits are generated one hold per step, settled before the comparison, and a hold across several flushes is not generated. |
| Includes and publication | Complete, [facade rollback](https://github.com/TanStack/db/blob/main/packages/db/tests/query/bucket-facade-rollback-oracle.property.test.ts) ([review](oracle-reviews/perf-lazy-bucket-rollback-copy.md)) | The central recomputation model plus cross-formulation, temporal demand, layered publication, Collection facade lifecycle and space bounds, route-context transport, functional projection, query-shape, optimistic, and source-work owners are literate. They keep the architecture document as their contract source. A failed facade flush restores every facade it wrote, disposes the facades it created, publishes no event or layout revision, and reads the stored rows of no other facade; nested facades across two edges have a pinned witness. The grammar adds optimistic facade inserts and optimistic deletes of shown graph rows; a retired facade must show exactly its visible optimistic rows, and every history ends by settling them and comparing all rows again. A facade commit that throws before committing is generated, and the restore must commit the open sync transaction. Every change pending at a failed root commit, child rows included, stays pending, and the next flush publishes it once. Open: the flush's edge and bucket map copy is unmeasured bookkeeping; disposal between a flush and its rollback has pinned witnesses only; held facade commits are generated one hold at a time for up to three flushes, and a release that covered earlier flushes checks rows only; the grammar also cleans up a shown facade from outside, lets optimistic rows share graph ids and later rolls them back, after which those keys are compared by value; a holder's cleanup of a shown facade rejects (code 241) and the facade keeps receiving the graph's writes; retiring a shown bucket without the graph's retractions is generated (`contradict`) and throws code 240; optimistic deletes and updates of shown graph rows are generated, and an update's copy keeps the row's key; a facade that `resolve()` created before activation can stay ready and empty after a failed flush (recorded limit). |
| Collection lifecycle | Complete | The shared logical-owner/acquisition-attempt/sync-run grammar plus mutation admission, lifecycle trace, publication, replay, disposal, and transaction-refinement boundaries are literate. |
| Paced mutations | Partial | The virtual-clock owner checks queue order, bounded admission, cleanup draining in FIFO/LIFO order at the configured wait, admitted writes after separate Collection cleanup, post-cleanup queue rejection with a disposal reason, pending debounce drain at the last call's quiet edge, pending trailing throttle drain at its regular edge, debounce and throttle edge options including omitted options, immediate rejection of dropped leading-only calls and both-disabled calls, caller-option immutability, and settlement. New debounce/throttle admission after cleanup, failed persistence, and broader generated schedules remain open. |
| Optimistic state | Complete | The independent base/intent/source-queue graph plus outcome, transaction-payload, and publication drivers are literate. |
Expand Down
20 changes: 20 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2419,3 +2419,23 @@ Development builds show this message, with `${...}` replaced by values:
```text
Include query is missing a captured parent projection for "${ref.path.join(`.`)}".
```

<a id="error-240"></a>

## Error 240: `query/live/bucket-facade-adapter.ts`

Development builds show this message, with `${...}` replaced by values:

```text
Bucket facade retired with rows the graph did not retract
```

<a id="error-241"></a>

## Error 241: `query/live/bucket-facade-adapter.ts`

Development builds show this message, with `${...}` replaced by values:

```text
A live query's child collection cannot be cleaned up directly. Clean up the live query instead.
```
5 changes: 5 additions & 0 deletions packages/db/src/collection/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,11 @@ export class CollectionStateManager<
this.commitPendingTransactions()
}

/** Whether the newest sync transaction is still open for writes. */
hasOpenSyncTransaction(): boolean {
return this.pendingSyncedTransactions.at(-1)?.committed === false
}

/** Every synced row once every accepted sync transaction applies. */
*acceptedSyncedEntries(): IterableIterator<[TKey, TOutput]> {
const { states, truncated } = this.pendingSyncedProjection
Expand Down
48 changes: 31 additions & 17 deletions packages/db/src/query/live/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,23 +610,37 @@ Route + BucketFacade -> CellValue(cell, Collection)

Parents sharing a bucket share its facade. Child changes update that Collection
without re-emitting every parent, and moving a route changes the parent field to
the destination bucket's facade. A facade is never retargeted to another
bucket. The D2 join retains inactive bucket rows and emits their current
snapshot when the bucket becomes active; the facade adapter does not buffer
discarded deltas. It keeps only the deltas a flush consumed until that flush
publishes: if the root commit fails, the adapter restores them, so the next
successful flush publishes each pending child change exactly once. The adapter retains a facade only while at least one parent
route uses its bucket. When the last route leaves, it deletes through sync every
key the facade still shows and drops its strong reference. The graph has
already retracted the bucket's rows, but a facade is a Collection: a user
transaction can show an optimistic row in it, and a sync commit can be held
behind a persisting transaction. Retirement is therefore a legal write to a
non-empty facade, not an invariant violation. It is a facade write of the
flush, so a failed flush restores it. A restore deletes every key the flush
wrote, including a write that a persisting transaction still holds, so the held
write cannot land after the rollback. An external holder may keep the retired
Collection alive, but a later active interval gets a new facade. Inline modes
do not create child Collections.
the destination bucket's facade. A facade is never retargeted to another bucket.
The D2 join retains inactive bucket rows and emits their current snapshot when
the bucket becomes active; the facade adapter does not buffer discarded deltas.
It keeps only the deltas a flush consumed until that flush publishes: if the
root commit fails, the adapter restores them, so the next successful flush
publishes each pending child change exactly once. The adapter retains a facade
only while at least one parent route uses its bucket. When the last route
leaves, it drops its strong reference. The graph has already retracted the
bucket's rows, but a facade is a Collection: a user transaction can show an
optimistic row in it, and a sync commit can be held behind a persisting
transaction. Retiring a facade that still shows such rows is legal, not an
invariant violation. Those rows have no synced row, so they leave when their
transactions settle. A row that the graph sent and never retracted is
different. The adapter reads each facade's projected synced rows: the synced
rows once every queued sync transaction applies, including a transaction a
persisting one holds and a transaction still open. It decides inserts, updates
and deletes from them rather than from the visible rows. A projected row left
at retirement is a
contradictory graph signal: the retirement throws, and the flush aborts and
restores every facade it wrote. The live query owns its facades:
several parent rows can share one, so a holder's `cleanup()` rejects (code 241).
The adapter disposes facades itself, when the live query is cleaned up and
when a failed flush removes the facades it created. Ordinary Collections keep
their cleanup and restart. A
restore undoes only the keys the
flush wrote, including writes that a persisting transaction still holds, so a
held write cannot land after the rollback. A facade that `resolve()` created
before its bucket activated can stay ready and empty after a failed flush; the
next flush writes its rows (a recorded limit). An external holder may keep the
retired Collection alive, but a later active interval gets a new facade. Inline
modes do not create child Collections.

Composition is pure. It constructs a new result along changed paths and does
not mutate a previously published row or use public routing metadata:
Expand Down
Loading
Loading