Skip to content

fix(db): compose optimistic updates from changes instead of whole-row snapshots - #1701

Open
marbemac wants to merge 1 commit into
TanStack:mainfrom
getdatanaut:mbm/optimistic-field-composition
Open

fix(db): compose optimistic updates from changes instead of whole-row snapshots#1701
marbemac wants to merge 1 commit into
TanStack:mainfrom
getdatanaut:mbm/optimistic-field-composition

Conversation

@marbemac

@marbemac marbemac commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Small fix to how the optimistic overlay composes rows.

Today an optimistic insert/update stores mutation.modified — the whole row as it looked at mutate() time — into optimisticUpserts. Since modified is built from the visible row (Object.assign({}, visibleRow, payload) in mutations.ts), whichever transaction sorts last dictates every field, including ones it never touched.

The clearest symptom: rolling back one of several in-flight transactions doesn't actually remove its change.

It doesn't change when committed sync gets applied. Sync still stays queued while a transaction is persisting, exactly as today. That's the half of #1630 that turned out to be unsound without temp/server key mapping, and it's not needed for any of this.

So this is only the projection half — same base as before, just composed per-field instead of replaced wholesale.

Related to RFC #1625.

// three transactions in flight on the same row
t1.mutate(() => collection.update(1, (d) => { d.a = `a1` }))
t2.mutate(() => collection.update(1, (d) => { d.b = `b2` }))
t3.mutate(() => collection.update(1, (d) => { d.c = `c3` }))
t1.commit(); t2.commit(); t3.commit()

t2.rollback()
// expected: { a: 'a1', b: 'b0', c: 'c3' }
// actual:   { a: 'a1', b: 'b2', c: 'c3' }   <- b2 is back

t3's snapshot was taken when b was already b2, so once t2 is gone t3 puts it right back. I don't think there's a reading of that which is correct.

The fix: update mutations now compose mutation.changes over the value they were layered on, so a mutation only owns the fields it actually changed. Inserts keep using modified, since an insert's changes deliberately omit schema defaults (there's a comment in mutations.ts saying as much).

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed concurrent optimistic updates so each transaction changes only the fields it modified.
    • Prevented rollbacks from overwriting unrelated updates or leaving stale values visible.
    • Preserved optimistic insert defaults when subsequent updates are applied.
    • Prevented redundant change notifications when an optimistic overlay produces no effective change.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e26dd938-b40d-4dee-ada3-fc6733579ea1

📥 Commits

Reviewing files that changed from the base of the PR and between 67c840f and a186187.

📒 Files selected for processing (3)
  • .changeset/soft-donkeys-shave.md
  • packages/db/src/collection/state.ts
  • packages/db/tests/optimistic-composition.test.ts

📝 Walkthrough

Walkthrough

Optimistic updates now merge only changed fields over the current row, preserving unrelated fields across concurrent transactions, sync commits, and rollbacks. Tests cover composition, defaults, synchronization, rollback isolation, and redundant event suppression.

Changes

Optimistic composition

Layer / File(s) Summary
Compose optimistic upserts
packages/db/src/collection/state.ts, .changeset/soft-donkeys-shave.md
CollectionStateManager resolves update mutations by merging changes with the current base row, caches valid compositions, and uses the resolver during optimistic recomputation and sync commits.
Validate overlay behavior
packages/db/tests/optimistic-composition.test.ts
Tests cover overlapping updates, unrelated sync changes, rollback isolation, retained schema defaults, and redundant event suppression.

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

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: composing optimistic updates from field-level changes instead of whole-row snapshots.
Description check ✅ Passed The description matches the template and includes changes, checklist, and release impact with testing and a changeset noted.
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.
✨ 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.

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.

1 participant