Conversation
|
Cursor Agent can help with this pull request. Just |
🦋 Changeset detectedLatest commit: 837286d The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-ivm
@tanstack/electric-db-collection
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: +3.05 kB (+3.32%) Total Size: 95 kB
ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 3.7 kB ℹ️ View Unchanged
|
…//github.com/TanStack/db into cursor/virtual-props-rfc-implementation-8087
Co-authored-by: sam.willis <sam.willis@gmail.com>
…//github.com/TanStack/db into cursor/virtual-props-rfc-implementation-8087
…//github.com/TanStack/db into cursor/virtual-props-rfc-implementation-8087
…//github.com/TanStack/db into cursor/virtual-props-rfc-implementation-8087
…//github.com/TanStack/db into cursor/virtual-props-rfc-implementation-8087
…//github.com/TanStack/db into cursor/virtual-props-rfc-implementation-8087
There was a problem hiding this comment.
We're adding a bunch of meta properties $key, $origin, etc. Each of these could conflict with user properties. I'd like to minimize potential conflicts by having a single $meta property and having all these props nested under it, so $meta.synced, $meta.origin, etc.
I reviewed this big PR with Claude. Here's our feedback:
Semantic concerns with virtual props in joins and aggregations
Nice work on the virtual props system — the core idea of $synced, $origin, $key, and $collectionId is really useful for single-collection queries. However, I think the "pass-through / add-if-missing" semantics break down in two important cases.
Joins: $collectionId becomes ambiguous
After a join, each result row is a composite of two (or more) collections:
q.from({ todo: todoCollection })
.join({ list: listCollection }, ({ todo, list }) => eq(list.id, todo.list_id), "inner")
.select(({ todo, list }) => ({
id: todo.id,
text: todo.text,
listName: list.name,
}))A single $collectionId string can't represent this — it will just carry forward whichever value was set first (likely the from source), silently losing provenance from the joined side. If consumers use $collectionId to route mutations back to the right collection, this is actively misleading rather than just incomplete.
Options:
- Make
$collectionIdan array or a map keyed by alias after a join (e.g.{ todo: "todo-collection-id", list: "list-collection-id" }) - Strip/nullify
$collectionIdon join results since it's now ambiguous - Keep it but rename it to make clear it only refers to the primary source
Aggregations: $synced, $origin, and $key lose their meaning
After a groupBy + aggregate, each result row represents a group of source rows, not a single entity:
q.from({ user: usersCollection })
.groupBy(({ user }) => user.departmentId)
.select(({ user }) => ({
departmentId: user.departmentId,
userCount: count(user.id),
avgAge: avg(user.age),
}))For this output:
$key: The aggregate row doesn't correspond to any single source row. Whatever key is passed through is arbitrary and meaningless.$synced: A group may contain a mix of synced and unsynced rows. Is the aggregatetrueonly if all rows in the group are synced? Or is it just pass-through from an arbitrary row? The difference matters hugely for UI that conditionally shows "saving..." indicators.$origin: Same problem — the group may mix'local'and'remote'rows.
The relax join and group-by type assertions commit and the +20% size bump in group-by.js suggest the implementation is patching around these issues by loosening types and carrying props through regardless, rather than addressing the semantic mismatch.
Suggestion
The pass-through pattern works well for operations that don't change row identity (where, orderBy, limit, select without aggregation). The problem is specifically joins (which merge identities) and groupBy (which destroys individual identity).
I'd suggest one of:
-
Strip or nullify virtual props after operations that break their semantics. Aggregation strips
$key/$synced/$origin; joins make$collectionIdan array or strip it. Reflect this in the types. -
Make virtual props conditional at the type level — a
from().where()result gets the full{ $synced, $origin, $key, $collectionId }, but agroupBy()result gets a narrower type that omits$key/$synced/$origin. More work, but more honest. -
At minimum, derive aggregate-level
$syncedmeaningfully — e.g.$synced = falseif any row in the group is unsynced. This would actually be useful ("some data in this group is still pending"). But$keyand$originstill don't have good aggregate semantics and should probably be omitted.
| if (groupByClause.length === 0) { | ||
| // For single-group aggregation, create a single group with all data | ||
| const aggregates: Record<string, any> = {} | ||
| const aggregates: Record<string, any> = { ...virtualAggregates } |
There was a problem hiding this comment.
Do we really need to make a copy?
|
|
||
| // Create aggregate functions for any aggregated columns in the SELECT clause | ||
| const aggregates: Record<string, any> = {} | ||
| const aggregates: Record<string, any> = { ...virtualAggregates } |
There was a problem hiding this comment.
Same, really needed to copy?
🎯 Changes
This PR implements the virtual properties system as described in the RFC. This introduces computed, read-only metadata fields prefixed with
$to every row, providing information about its sync status, origin, key, and source collection ID.Key aspects of this implementation:
$synced: Indicates whether a row reflects confirmed state from the backend (true) or has pending optimistic mutations (false).$origin: Specifies if the last confirmed change originated from the local client ('local') or was received via sync ('remote').$key: The primary identifier of the row.$collectionId: The ID of the collection the row originated from.where(({ item }) => eq(item.$synced, true))).RefProxyandReftypes to include virtual properties, ensuring type safety and discoverability.This feature enhances data visibility and enables more sophisticated UI logic based on the state and origin of data.
✅ Checklist
pnpm test:pr.🚀 Release Impact