Skip to content

fix(query-db): clean up empty ownership sets#1672

Open
KyleAMathews wants to merge 9 commits into
mainfrom
query-ownership-empty-set-cleanup
Open

fix(query-db): clean up empty ownership sets#1672
KyleAMathews wants to merge 9 commits into
mainfrom
query-ownership-empty-set-cleanup

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Cleans up Query DB ownership bookkeeping so empty row/query ownership sets are not retained, while preserving the semantic distinction between an unresolved query and a resolved query with an empty result.

This keeps ownership state bounded across subset unloads, cache expiry, persisted-row hydration, and empty revalidation without deleting rows that are still shared by another query.

Design

Root cause

rowToQueries and queryToRows used empty Sets both as stale bookkeeping and as a marker that a query's ownership had been resolved. Removing those empty entries directly would lose the resolved-empty baseline and could cause hydration or revalidation to infer ownership again incorrectly.

Approach

  • Delete reverse and forward ownership-map entries as soon as their sets become empty.
  • Track authoritative query ownership separately in resolvedOwnershipQueries, including valid empty results.
  • Clear that marker when retained ownership expires or the query/collection is cleaned up.
  • Add test-only ownership inspection and lifecycle coverage for overlapping subsets, cache GC, persisted hydration, empty revalidation, and retained ownership expiry.

Invariants

  • Ownership maps contain only non-empty sets.
  • A row is removed only after its final query owner is removed.
  • A resolved empty result remains authoritative until its query lifecycle ends.
  • Persisted retention markers and in-memory ownership markers expire together.

Non-goals and tradeoffs

  • No public API or persistence-format changes.
  • The separate resolved-query set adds a small amount of state, but avoids overloading empty map entries and is explicitly bounded by existing cleanup paths.
  • Internal ownership-map inspection is exposed only in test environments.

Verification

  • pnpm --filter @tanstack/query-db-collection test -- query.test.ts
    • 232 tests passed and type assertions reported no errors in the target suite.
    • Command exits non-zero because Vitest also type-checks db-collection-e2e sources that cannot resolve @tanstack/electric-db-collection in this worktree (3 unrelated source errors).

Files

  • packages/query-db-collection/src/query.ts — compact ownership maps and preserve resolved-empty semantics with bounded lifecycle tracking.
  • packages/query-db-collection/tests/query.test.ts — characterize and verify ownership cleanup, hydration, shared rows, cache expiry, and retained-query cleanup.

Summary by CodeRabbit

  • Bug Fixes

    • Improved cleanup of query and row ownership state when data is unloaded, expires, or revalidates to an empty result.
    • Preserved shared rows while removing data no longer owned by any active query.
    • Ensured retained rows are removed correctly after their retention period ends.
  • Tests

    • Added coverage for ownership cleanup, cache expiration, persisted data hydration, empty query results, and retention lifecycles.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Query ownership bookkeeping now distinguishes resolved empty results from absent state, removes empty row relationships, clears persisted ownership markers, exposes test-only ownership maps, and adds lifecycle coverage for unloading, expiry, revalidation, and retention cleanup.

Changes

Query ownership lifecycle

Layer / File(s) Summary
Ownership bookkeeping primitives
packages/query-db-collection/src/query.ts
Resolved query baselines can contain empty sets, empty row ownership entries are removed, missing ownership data is handled safely, and test-only ownership-map inspection is exposed.
Persisted ownership cleanup
packages/query-db-collection/src/query.ts
Persisted placeholder cleanup removes the resolved ownership marker for the query.
Ownership lifecycle validation
packages/query-db-collection/tests/query.test.ts, .changeset/clean-query-ownership.md
Tests cover overlapping unloads, cache expiry, retained-row revalidation, and TTL cleanup; a patch changeset documents the update.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

Possibly related PRs

  • TanStack/db#1664 — Both modify ownership helper semantics, including addRowOwners and removeRowOwner.
  • TanStack/db#1665 — Both modify enhancedInternalSync and intersect in query lifecycle cleanup behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately captures the main change: cleaning up empty ownership sets.
Description check ✅ Passed The description is thorough and covers the change, design, verification, and release impact, though it doesn't follow the exact template headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch query-ownership-empty-set-cleanup

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 13, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1672

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1672

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1672

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1672

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1672

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1672

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1672

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1672

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1672

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1672

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1672

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1672

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1672

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1672

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1672

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1672

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1672

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1672

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1672

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1672

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1672

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1672

commit: d2d5f8e

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 125 kB

ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/collection/change-events.js 1.43 kB
packages/db/dist/esm/collection/changes.js 1.38 kB
packages/db/dist/esm/collection/cleanup-queue.js 810 B
packages/db/dist/esm/collection/events.js 434 B
packages/db/dist/esm/collection/index.js 3.62 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 1.69 kB
packages/db/dist/esm/collection/mutations.js 2.47 kB
packages/db/dist/esm/collection/state.js 5.48 kB
packages/db/dist/esm/collection/subscription.js 3.74 kB
packages/db/dist/esm/collection/sync.js 2.88 kB
packages/db/dist/esm/collection/transaction-metadata.js 144 B
packages/db/dist/esm/deferred.js 207 B
packages/db/dist/esm/errors.js 5.1 kB
packages/db/dist/esm/event-emitter.js 748 B
packages/db/dist/esm/index.js 3.16 kB
packages/db/dist/esm/indexes/auto-index.js 829 B
packages/db/dist/esm/indexes/base-index.js 767 B
packages/db/dist/esm/indexes/basic-index.js 2.06 kB
packages/db/dist/esm/indexes/btree-index.js 2.19 kB
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 557 B
packages/db/dist/esm/live-query-adapter.js 318 B
packages/db/dist/esm/local-only.js 916 B
packages/db/dist/esm/local-storage.js 2.12 kB
packages/db/dist/esm/optimistic-action.js 359 B
packages/db/dist/esm/paced-mutations.js 496 B
packages/db/dist/esm/proxy.js 3.75 kB
packages/db/dist/esm/query/builder/functions.js 1.47 kB
packages/db/dist/esm/query/builder/index.js 5.84 kB
packages/db/dist/esm/query/builder/ref-proxy.js 1.24 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.89 kB
packages/db/dist/esm/query/compiler/expressions.js 430 B
packages/db/dist/esm/query/compiler/group-by.js 3.56 kB
packages/db/dist/esm/query/compiler/index.js 6.67 kB
packages/db/dist/esm/query/compiler/joins.js 2.5 kB
packages/db/dist/esm/query/compiler/lazy-targets.js 923 B
packages/db/dist/esm/query/compiler/order-by.js 1.74 kB
packages/db/dist/esm/query/compiler/select.js 1.53 kB
packages/db/dist/esm/query/effect.js 4.77 kB
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir.js 1.25 kB
packages/db/dist/esm/query/live-query-collection.js 360 B
packages/db/dist/esm/query/live/collection-config-builder.js 9.1 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 1.93 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/utils.js 1.81 kB
packages/db/dist/esm/query/optimizer.js 2.92 kB
packages/db/dist/esm/query/predicate-utils.js 2.97 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/subset-dedupe.js 960 B
packages/db/dist/esm/scheduler.js 1.3 kB
packages/db/dist/esm/SortedMap.js 1.3 kB
packages/db/dist/esm/strategies/debounceStrategy.js 247 B
packages/db/dist/esm/strategies/queueStrategy.js 428 B
packages/db/dist/esm/strategies/throttleStrategy.js 246 B
packages/db/dist/esm/transactions.js 3.04 kB
packages/db/dist/esm/utils.js 927 B
packages/db/dist/esm/utils/array-utils.js 273 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 5.61 kB
packages/db/dist/esm/utils/comparison.js 1.11 kB
packages/db/dist/esm/utils/cursor.js 457 B
packages/db/dist/esm/utils/index-optimization.js 2.39 kB
packages/db/dist/esm/utils/type-guards.js 157 B
packages/db/dist/esm/utils/uuid.js 449 B
packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 4.22 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/index.js 249 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
packages/react-db/dist/esm/useLiveQuery.js 1.33 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 567 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/query-db-collection/src/query.ts`:
- Line 1656: Update collection cleanup to build its key set from
state.observers, queryToRows, and resolvedOwnershipQueries so retained ownership
queries are removed before TTL expiry or revalidation; preserve deletion of
queryToRows, rowToQueries, and resolvedOwnershipQueries for every collected key.
Add a regression test covering collection.cleanup() while a retained placeholder
is pending, asserting all three structures are empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 225651f4-0c6e-4d6d-b6cc-a51d4b9b8966

📥 Commits

Reviewing files that changed from the base of the PR and between 09d7d67 and 29eefca.

📒 Files selected for processing (4)
  • .changeset/clean-query-ownership.md
  • .superpowers/sdd/task-2-report.md
  • packages/query-db-collection/src/query.ts
  • packages/query-db-collection/tests/query.test.ts


state.observers.delete(hashedQueryKey)
queryToRows.delete(hashedQueryKey)
resolvedOwnershipQueries.delete(hashedQueryKey)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include retained ownership keys in explicit collection cleanup.

Line 1656 is never reached for retained queries removed from state.observers at Line 1726. Since cleanup() only iterates observer keys, cleanup before TTL expiry/revalidation leaves queryToRows, rowToQueries, and resolvedOwnershipQueries retained.

Build the cleanup key set from state.observers, queryToRows, and resolvedOwnershipQueries. Add a regression test that calls collection.cleanup() while a retained placeholder is still pending and asserts all three structures are empty.

As per coding guidelines, “Always add unit tests that reproduce a bug before fixing it to ensure the bug is fixed and prevent regression.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/query-db-collection/src/query.ts` at line 1656, Update collection
cleanup to build its key set from state.observers, queryToRows, and
resolvedOwnershipQueries so retained ownership queries are removed before TTL
expiry or revalidation; preserve deletion of queryToRows, rowToQueries, and
resolvedOwnershipQueries for every collected key. Add a regression test covering
collection.cleanup() while a retained placeholder is pending, asserting all
three structures are empty.

Source: Coding guidelines

@kevin-dp kevin-dp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lifecycle fix (deleting stale entries at the right points) looks right to me. I'd like to suggest a simplification before this lands, though: I don't think we need resolvedOwnershipQueries as a separate structure.

The marker is only ever consulted as knownRows || resolvedOwnershipQueries.has(k),
i.e. a non-empty queryToRows entry already counts as authoritative on its own. The
new set only carries information when the row set is empty — which means we can encode "resolved" as entry presence in queryToRows itself, with an empty Set allowed:

  • present entry (possibly empty) = ownership resolved
  • absent entry = unresolved

Concretely:

  • At the resolution point, replace resolvedOwnershipQueries.add(k) with queryToRows.set(k, queryToRows.get(k) ?? new Set()).
  • In removeRowOwner, keep the drained queryToRows entry instead of deleting it — a query whose rows are diffed away during revalidation is still resolved and now legitimately owns nothing. (The delete-when-empty stays correct for rowToQueries, since a row with no owners has no "resolved" meaning.)
  • The two teardown paths (cleanupPersistedPlaceholder and query cleanup) then just delete the one entry instead of clearing two structures.

The read side reverts to the simpler pre-PR shape (if (knownRows)), and the compound (knownRows || resolvedOwnershipQueries.has(k)) && Array.from(knownRows ?? []) check goes away. More importantly, there's one source of truth: right now three separate mutation sites have to keep resolvedOwnershipQueries in sync with the map's lifecycle, and a missed site in a future change becomes a silent drift bug.

The trade-off is that the invariant becomes implicit — an accidental get-or-create on
queryToRows (like the existing addRowOwner pattern) would silently mark an unresolved query as resolved-empty. I think that's addressable by funneling writes through a couple of small helpers, documenting the presence-means-resolved invariant on the map declaration, and asserting it via the test-only inspection hook this PR already adds.

Memory-wise it's equivalent, so this is purely about keeping the bookkeeping in one place. WDYT?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/query-db-collection/src/query.ts`:
- Around line 740-741: Update cleanup() to build its query-key set from both
state.observers.keys() and queryToRows keys, then use that combined set when
removing ownership data so retained queries are fully cleared from queryToRows
and rowToQueries during explicit collection cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a3e7916-3963-40c6-9263-cbd68dc9b5ae

📥 Commits

Reviewing files that changed from the base of the PR and between 29eefca and d2d5f8e.

📒 Files selected for processing (2)
  • packages/query-db-collection/src/query.ts
  • packages/query-db-collection/tests/query.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/query-db-collection/tests/query.test.ts

Comment on lines +740 to +741
// queryKey → Set<RowKey>. Entry presence means ownership is resolved;
// an empty set represents a resolved query that currently owns no rows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Include retained ownership keys in explicit collection cleanup.

As flagged in a previous review, queryToRows retains ownership keys for queries removed from state.observers during retention. The cleanup() method still only iterates state.observers.keys(), leaving queryToRows and rowToQueries populated for retained queries when an explicit collection cleanup occurs.

Build the cleanup key set by combining state.observers and queryToRows to ensure retained queries are properly cleaned up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/query-db-collection/src/query.ts` around lines 740 - 741, Update
cleanup() to build its query-key set from both state.observers.keys() and
queryToRows keys, then use that combined set when removing ownership data so
retained queries are fully cleared from queryToRows and rowToQueries during
explicit collection cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants