Skip to content

fix(db): avoid full row origin snapshots#1640

Merged
samwillis merged 4 commits into
TanStack:mainfrom
samwillis:codex/fix-row-origin-snapshot-main
Jul 2, 2026
Merged

fix(db): avoid full row origin snapshots#1640
samwillis merged 4 commits into
TanStack:mainfrom
samwillis:codex/fix-row-origin-snapshot-main

Conversation

@samwillis

@samwillis samwillis commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes two collection/transaction performance hot paths:

  • Avoids source-sized rowOrigins snapshots during incremental collection updates.
  • Makes Transaction.applyMutations merge bulk mutations in O(n) instead of O(n²).

Root Cause

Single-row incremental writes were cloning the full rowOrigins map before propagating changes. Since rowOrigins grows with the source collection, normal incremental writes paid O(source collection size) work before the query graph could process the actual changed keys.

Kevin also identified a separate bulk mutation issue: Transaction.applyMutations used findIndex for every incoming mutation, making large bulk operations quadratic.

Changes

  • Reuse the existing rowOrigins map during optimistic recompute, where confirmed origins are not mutated.
  • Snapshot row origins only for keys affected by a synced commit, plus pending direct optimistic keys that may be cleaned up by sync confirmation.
  • Merge transaction mutations through a globalKey-keyed Map, then rebuild the public mutations array in place to preserve identity.
  • Keep the PR branch free of tracing or benchmark instrumentation.

Validation

  • pnpm --dir packages/db exec tsc --noEmit
  • pnpm --dir packages/db exec vitest --run tests/transactions.test.ts tests/collection-subscribe-changes.test.ts tests/collection-change-events.test.ts tests/collection-truncate.test.ts

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

applyMutations now uses a Map keyed by globalKey to merge pending mutations and rebuilds the array in place. A changeset entry records a patch release and notes the row-origin and bulk merge updates.

Changes

Bulk mutation merge update

Layer / File(s) Summary
Map-based mutation merging
packages/db/src/transactions.ts
applyMutations merges pending mutations through a Map, deletes entries when merges resolve to null, and repopulates this.mutations in place.
Release note entry
.changeset/quiet-row-origins.md
The changeset marks @tanstack/db for a patch release and documents the row-origin and bulk merge behavior changes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: KyleAMathews

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main performance fix, though it only names one part of the broader changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description check ✅ Passed The PR description is detailed and on-topic, with clear summary, root cause, changes, and validation, but it omits the checklist and release impact sections.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 2, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

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

@tanstack/browser-db-sqlite-persistence

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

@tanstack/capacitor-db-sqlite-persistence

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

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

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/db-sqlite-persistence-core

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

@tanstack/electric-db-collection

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

@tanstack/electron-db-sqlite-persistence

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

@tanstack/expo-db-sqlite-persistence

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

@tanstack/node-db-sqlite-persistence

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

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

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/tauri-db-sqlite-persistence

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: 02889ef

@samwillis samwillis marked this pull request as ready for review July 2, 2026 11:02
@samwillis samwillis changed the title [codex] avoid full row origin snapshots fix(db): avoid full row origin snapshots Jul 2, 2026
@samwillis samwillis requested a review from kevin-dp July 2, 2026 11:05
@kevin-dp

kevin-dp commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

We hit the same root cause in a parallel investigation (profiled the two new Map(this.rowOrigins) clones at >80% of per-write CPU at 50k rows) and ended up with a functionally equivalent fix — copy-on-write overlay instead of an eager bounded snapshot. Benchmarked both head-to-head: within noise, single-row update on a 50k-row collection goes ~9.5ms → ~0.18ms either way. So 👍 on this fix as-is.

One additional, independent hotspot we found along the way that this PR doesn't cover: Transaction.applyMutations is O(n²) for bulk mutations.

Problem/cause: applyMutations merges each incoming mutation via this.mutations.findIndex((m) => m.globalKey === newMutation.globalKey) — a linear scan of the already-accepted mutations for every incoming one. A bulk collection.insert(rows) with 50k rows is a single call with 50k mutations, so the k-th mutation scans ~k existing ones: ≈1.25 billion key comparisons total. In our profiling, seeding a 50k-row collection through the optimistic path took tens of seconds, with the majority of CPU inside applyMutations. (This doesn't affect the per-write numbers in the external benchmark — single-row transactions only ever scan a 1-element array — it bites bulk inserts/updates and initial data loads.)

Fix: merge through a globalKey-keyed Map instead of scanning. Map preserves insertion order and set() on an existing key keeps its original position, so the existing replace-in-place / cancel-out / append semantics are unchanged; the mutations array is rebuilt in place to preserve its identity since it's a public field. O(n²) → O(n) per call.

Commit: kevin-dp/db@5b900cd (on our investigation branch, perf/avoid-per-write-row-origins-clone). Happy to open it as a separate PR — probably cleaner than piggybacking on this one.

kevin-dp and others added 2 commits July 2, 2026 14:30
applyMutations did a findIndex scan over the existing mutations array for
every incoming mutation, making bulk operations quadratic: a single
insert() of 50k rows spent most of its time in this scan. Merge through a
globalKey-keyed Map instead, preserving insertion order and rebuilding the
array in place to keep its identity for external holders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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/db/src/transactions.ts (1)

336-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a regression test for bulk-merge ordering/semantics.

This function's correctness now hinges on subtle Map insertion-order behavior (replace keeps position, delete-then-reinsert moves to end) across insert/update/delete combinations. A dedicated test exercising bulk mutations with repeated globalKeys (including cancel-out and reorder cases) would guard against regressions in this performance-critical path.

🤖 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/db/src/transactions.ts` around lines 336 - 370, Add a regression
test for Transactions.applyMutations that covers bulk mutation merging with
repeated globalKey values, including update/replace, delete-cancels-insert, and
delete-then-reinsert ordering cases. Exercise the applyMutations method directly
with a sequence of PendingMutation entries and assert both the final mutations
contents and their preserved order to lock in the current Map-based semantics.
🤖 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/db/src/transactions.ts`:
- Around line 336-370: Add a regression test for Transactions.applyMutations
that covers bulk mutation merging with repeated globalKey values, including
update/replace, delete-cancels-insert, and delete-then-reinsert ordering cases.
Exercise the applyMutations method directly with a sequence of PendingMutation
entries and assert both the final mutations contents and their preserved order
to lock in the current Map-based semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c92499c-8979-428c-8293-cd914e4e6bfd

📥 Commits

Reviewing files that changed from the base of the PR and between 73d64e4 and 02889ef.

📒 Files selected for processing (2)
  • .changeset/quiet-row-origins.md
  • packages/db/src/transactions.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/quiet-row-origins.md

@samwillis samwillis merged commit 397e12a into TanStack:main Jul 2, 2026
11 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 2, 2026
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