Skip to content

feat: $pendingOperation virtual prop#1431

Open
marbemac wants to merge 6 commits intoTanStack:mainfrom
marbemac:mbm/pending-operation-virtual-prop
Open

feat: $pendingOperation virtual prop#1431
marbemac wants to merge 6 commits intoTanStack:mainfrom
marbemac:mbm/pending-operation-virtual-prop

Conversation

@marbemac
Copy link
Copy Markdown
Contributor

@marbemac marbemac commented Mar 30, 2026

🎯 Changes

Adds a new $pendingOperation virtual property to collection rows ('insert' | 'update' | 'delete' | null). This tells you what type of optimistic mutation is pending for each row, which is useful for building draft/review UIs where you want to show git-style change indicators, amongst other things.

Note, large-ish diff but most of it is new tests 😅.

The key feature: items deleted in a pending transaction can now stay visible in query results. By default nothing changes — deleted items still vanish from queries. But if your query references $pendingOperation in a .where() clause, the implicit filter is disabled and you can see pending-delete items inline:

// Default — deleted items hidden (same as before)
q.from({ task: tasks }).where(({ task }) => eq(task.projectId, projectId))

// Opt in — deleted items visible with $pendingOperation: 'delete'
q.from({ task: tasks }).where(({ task }) =>
  and(
    eq(task.projectId, projectId),
    or(isNull(task.$pendingOperation), not(isNull(task.$pendingOperation))),
  ),
)

Works with live queries, createEffect, joins/subqueries, GROUP BY, ordered/paginated queries, and selective where clauses like not(isNull($pendingOperation)) for "show only pending changes" views.

This was discussed with @samwillis beforehand — the approach is to keep deletes as deletes at the collection layer and convert them to updates at the subscription layer when opted in.

I also went ahead and built and linked into our actual app to test out the functionality. It's damn neat! For example, can show indicators inline for as yet uncommitted transaction. Imagine CRM app, and app with data grids w rows that can be updated/removed/etc, or really any productivity app that wants to allow humans and agents to safely make changes, and represent those changes inline in the application without having to change the data layer at all (note R and A badges on the files on the left, and the red bordered "install" that has been deleted but not yet "committed").

Screenshot 2026-03-29 at 7 45 41 PM

Notable behavioral change

isRowSynced() now also checks pendingOptimisticUpserts and pendingOptimisticDeletes, so $synced is false during the completed-but-awaiting-sync window. Previously it could briefly show $synced: true while a pending operation was still awaiting sync confirmation.

✅ 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).

marbemac and others added 6 commits March 29, 2026 19:41
… type

Add a new virtual property $pendingOperation ('insert' | 'update' | 'delete' | null)
to every collection row. This tells consumers what type of optimistic mutation is
pending, enabling draft/review UIs that show git-style change indicators.
- Subscription-layer delete-to-update conversion for opted-in queries
- Auto-detection of $pendingOperation in where clauses
- Initial snapshot includes pending-delete items
- Fix missing $pendingOperation in multi-group GROUP BY path
- Tighten string|null to PendingOperationType in group-by compiler
- Fix ?? vs !== undefined inconsistency for nullable $pendingOperation
- Add GROUP BY tests for $pendingOperation aggregation
… memory cleanup

- Fix stale $pendingOperation after rollback of optimistic delete by tracking
  converted delete values and converting rollback inserts to updates
- Fix isRowSynced to check pendingOptimistic* maps for consistency with
  getPendingOperation (prevents contradictory $synced: true + $pendingOperation: 'delete')
- Fix lazy source (join/subquery) pending deletes by merging child
  sourceWhereClauses into parent during query compilation
- Fix convertedDeleteValues memory leak: clean up on sync-confirmed delete,
  truncate, and unsubscribe
- Add computePendingOperation callback to enrichRowWithVirtualProps
Export expressionReferencesPendingOperation from collection-subscriber
and use it in effect.ts buildSubscriptionOptions to auto-detect
$pendingOperation references and set includePendingDeletes on the
subscription.
Signed-off-by: Marc MacLeod <marbemac+gh@gmail.com>
@marbemac
Copy link
Copy Markdown
Contributor Author

marbemac commented Mar 30, 2026

One thing I considered but left out because wasn't sure if ya'll would be ok w a new API method. A .includePendingDeletes() method on the query builder. Since the tautology pattern is a little awkward for the "include everything including deleted items" use case, instead of:

.where(({ task }) =>
  and(
    eq(task.projectId, projectId),
    or(isNull(task.$pendingOperation), not(isNull(task.$pendingOperation))),
  ),
)

User could do:

.where(({ task }) => eq(task.projectId, projectId))
.includePendingDeletes()

OR alternative, just a simple exported util:

.where(({ task }) => includeDeletedItems(eq(task.projectId, projectId)))

If that's something you guys would consider, I can try and add it - don't think it'd be much code.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 30, 2026

More templates

@tanstack/angular-db

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

@tanstack/browser-db-sqlite-persistence

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

@tanstack/capacitor-db-sqlite-persistence

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

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

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/db-sqlite-persistence-core

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

@tanstack/electric-db-collection

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

@tanstack/electron-db-sqlite-persistence

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

@tanstack/expo-db-sqlite-persistence

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

@tanstack/node-db-sqlite-persistence

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

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

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/tauri-db-sqlite-persistence

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: afb1604

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