Skip to content

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

Merged
KyleAMathews merged 10 commits into
mainfrom
query-ownership-empty-set-cleanup
Jul 20, 2026
Merged

fix(query-db): clean up empty ownership sets#1672
KyleAMathews merged 10 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 and synchronization of query↔row ownership when data is unloaded, expires, revalidates, or hydrates from persisted metadata.
    • Empty query results now establish an authoritative ownership baseline without incorrectly deleting shared/retained rows.
    • Retained-row ownership is cleared reliably after retention ends and when an explicit collection cleanup runs.
  • Tests

    • Added deeper lifecycle coverage for ownership mapping across overlapping subsets, cache expiry, persisted hydration, empty-result revalidation, and retention cleanup.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Query ownership bookkeeping now preserves authoritative empty results, removes empty ownership relationships, clears persisted markers, and validates unloading, expiry, revalidation, retention, and collection cleanup.

Changes

Query ownership lifecycle

Layer / File(s) Summary
Ownership bookkeeping and result reconciliation
packages/query-db-collection/src/query.ts
Resolved queries may own an empty set, empty row relationships are deleted, successful results establish ownership baselines, and test-only map inspection is exposed.
Persisted ownership cleanup
packages/query-db-collection/src/query.ts
Persisted placeholder cleanup clears resolved ownership markers, and collection cleanup includes tracked ownership keys.
Ownership lifecycle validation
packages/query-db-collection/tests/query.test.ts, .changeset/clean-query-ownership.md
Tests cover unload, cache expiry, retained-row revalidation, TTL cleanup, and explicit collection cleanup; a patch changeset records the release.

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

Possibly related issues

Possibly related PRs

  • TanStack/db#1664 — Both modify query.ts ownership helper semantics and query lifecycle cleanup.
  • TanStack/db#1626 — Both address stale ownership metadata during persistence and hydration flows.

Suggested reviewers: kevin-dp

🚥 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 summarizes the main change: cleaning up empty ownership sets in query-db.
Description check ✅ Passed The description is detailed and covers the change, design, verification, and release impact, though it does not follow the repo template headings exactly.
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

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: b9a204f

@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 thread packages/query-db-collection/src/query.ts

@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.

🧹 Nitpick comments (1)
packages/query-db-collection/tests/query.test.ts (1)

5974-5977: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Spread originalSync to preserve all SyncPlugin properties.

When intercepting the sync method of the originalSync plugin, it's safer to spread the rest of the originalSync properties into the new object. Although core lifecycle methods like cleanup and loadSubset may be returned dynamically in the SyncResult, spreading the plugin object ensures any top-level properties (like id or future plugin hooks) are not inadvertently lost.

♻️ Proposed refactor
         sync: {
+          ...originalSync,
           sync: (params: Parameters<typeof originalSync.sync>[0]) =>
             originalSync.sync({ ...params, metadata: metadataHarness.api }),
         },
🤖 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/tests/query.test.ts` around lines 5974 - 5977,
Update the intercepted originalSync plugin object to spread all properties from
originalSync before overriding its sync method, preserving top-level SyncPlugin
fields such as id and future hooks while retaining the metadata injection
behavior.
🤖 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.

Nitpick comments:
In `@packages/query-db-collection/tests/query.test.ts`:
- Around line 5974-5977: Update the intercepted originalSync plugin object to
spread all properties from originalSync before overriding its sync method,
preserving top-level SyncPlugin fields such as id and future hooks while
retaining the metadata injection behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b61115a-4091-4b21-9a11-d4033311eb60

📥 Commits

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

📒 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/src/query.ts

@KyleAMathews
KyleAMathews merged commit 932910d into main Jul 20, 2026
11 checks passed
@KyleAMathews
KyleAMathews deleted the query-ownership-empty-set-cleanup branch July 20, 2026 14:35
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