Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/docs-feedback-clarifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/db': patch
---

Clarify documentation for caseWhen, coalesce, manual transactions, and multi-endpoint Query Collection behavior. Add utility function categorization and fix reference index ordering.
21 changes: 21 additions & 0 deletions docs/collections/query-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,27 @@ The query collection treats the `queryFn` result as the **complete state** of th
- Items in the query result but not in the collection will be inserted
- Items present in both will be updated if they differ

This is important when the same entity type can be loaded from multiple REST
endpoints. For example, do not point the same Query Collection at
`/api/documents/preview` for one load and `/api/documents/deleted` for another
unless each result represents the complete state for that collection
scope. A narrower endpoint can otherwise remove rows that were loaded from a
different endpoint.

For multiple endpoint or subset-loading use cases, choose the pattern that
matches your API semantics:

- Use `syncMode: 'on-demand'` when one logical collection can serve different
subsets of data. In this mode, query predicates (`where`, `orderBy`, `limit`,
and `offset`) are passed to your `queryFn` via `ctx.meta.loadSubsetOptions`,
letting you translate them into API parameters.
- Use separate Query Collections when endpoints represent distinct server scopes
whose results should not replace each other. Use `unionAll` to combine them
into a single query when you need a unified view across endpoints.
- Use direct writes such as `writeUpsert`/`writeBatch` for lower-level
incremental loading when you intentionally want to merge server responses into
the synced store yourself.

### Empty Array Behavior

When `queryFn` returns an empty array, **all items in the collection will be deleted**. This is because the collection interprets an empty array as "the server has no items".
Expand Down
47 changes: 46 additions & 1 deletion docs/guides/live-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -2551,12 +2551,57 @@ Add two numbers:
add(user.salary, user.bonus)
```

### Utility Functions

#### `coalesce(...values)`
Return the first non-null value:
Return the first non-null/undefined value:
```ts
coalesce(user.displayName, user.name, 'Unknown')
```

This is useful for display fallbacks when the stored value is `null` or
`undefined`:

```ts
.select(({ document }) => ({
...document,
displayTitle: coalesce(document.title, 'Untitled document'),
}))
```

If you also need to treat another value, such as an empty string, as missing,
use `caseWhen` to express that condition explicitly.

#### `caseWhen(condition, value, ...)`
Return the value for the first matching condition, similar to SQL `CASE WHEN`.
Arguments are provided as condition/value pairs followed by an optional default
value:

```ts
caseWhen(gt(user.age, 65), 'senior', gt(user.age, 18), 'adult', 'minor')
```

If no condition matches and no default value is provided, scalar expressions
return `null`.

Use `caseWhen` when a computed field needs conditional logic that cannot be
represented with `coalesce` alone. For example, to display a fallback for both
`null`/`undefined` and empty-string titles:

```ts
.select(({ document }) => ({
...document,
displayTitle: caseWhen(
eq(coalesce(document.title, ''), ''),
'Untitled document',
document.title,
),
}))
```

`caseWhen` can also be used in expression contexts such as `select`, `where`,
`orderBy`, `groupBy`, `having`, and equality join operands.

### Aggregate Functions

#### `count(value)`
Expand Down
14 changes: 14 additions & 0 deletions docs/guides/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,20 @@ await tx.commit()
tx.rollback()
```

When you call collection methods inside `tx.mutate()`, the mutations are
captured by the manual transaction. The collection's `onInsert`, `onUpdate`, and
`onDelete` handlers are not invoked for those mutations; the
manual transaction's `mutationFn` is responsible for persisting
`transaction.mutations`.

This makes manual transactions a good fit for draft-style workflows where local
state should update immediately, but persistence should wait for a later user
action such as Save or Blur. Each `tx.mutate()` call updates the optimistic
state instantly, so the UI reflects changes as the user types. When the user
triggers Save, `tx.commit()` fires the `mutationFn` to persist all accumulated
mutations in a single batch. If the user cancels instead, `tx.rollback()`
discards the optimistic changes and reverts the UI.

### Multi-Step Workflows

Manual transactions excel at complex workflows:
Expand Down
1 change: 1 addition & 0 deletions docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ title: "@tanstack/db"
- [add](functions/add.md)
- [and](functions/and.md)
- [avg](functions/avg.md)
- [caseWhen](functions/caseWhen.md)
- [clearQueryPatterns](functions/clearQueryPatterns.md)
- [coalesce](functions/coalesce.md)
- [compileExpression](functions/compileExpression.md)
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/variables/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ title: operators
# Variable: operators

```ts
const operators: readonly ["eq", "gt", "gte", "lt", "lte", "in", "like", "ilike", "and", "or", "not", "isNull", "isUndefined", "upper", "lower", "length", "concat", "add", "coalesce", "count", "avg", "sum", "min", "max"];
const operators: readonly ["eq", "gt", "gte", "lt", "lte", "in", "like", "ilike", "and", "or", "not", "isNull", "isUndefined", "upper", "lower", "length", "concat", "add", "coalesce", "caseWhen", "count", "avg", "sum", "min", "max"];
```

Defined in: [packages/db/src/query/builder/functions.ts:403](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts#L403)
Defined in: [packages/db/src/query/builder/functions.ts](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts)

All supported operator names in TanStack DB expressions
37 changes: 34 additions & 3 deletions packages/db/skills/db-core/live-queries/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ description: >
fullJoin, select, fn.select, groupBy, having, orderBy, limit, offset, distinct,
findOne. Operators: eq, gt, gte, lt, lte, like, ilike, inArray, isNull,
isUndefined, and, or, not. Aggregates: count, sum, avg, min, max. String
functions: upper, lower, length, concat, coalesce. Math: add. $selected
namespace. createLiveQueryCollection. Derived collections. Predicate push-down.
functions: upper, lower, length, concat. Utility: coalesce, caseWhen. Math: add.
$selected namespace. createLiveQueryCollection. Derived collections. Predicate push-down.
Incremental view maintenance via differential dataflow (d2ts). Virtual
properties ($synced, $origin, $key, $collectionId). Includes subqueries
for hierarchical data. toArray and concat(toArray(...)) scalar includes.
Expand Down Expand Up @@ -385,7 +385,7 @@ const { data } = useLiveQuery((q) =>

### HIGH: Not using the full operator set

The library provides string functions (`upper`, `lower`, `length`, `concat`), math (`add`), utility (`coalesce`), and aggregates (`count`, `sum`, `avg`, `min`, `max`). All are incrementally maintained. Prefer them over JS equivalents.
The library provides string functions (`upper`, `lower`, `length`, `concat`), math (`add`), utility functions (`coalesce`, `caseWhen`), and aggregates (`count`, `sum`, `avg`, `min`, `max`). All are incrementally maintained. Prefer them over JS equivalents.

```ts
// WRONG
Expand All @@ -398,9 +398,40 @@ The library provides string functions (`upper`, `lower`, `length`, `concat`), ma
.select(({ user, order }) => ({
name: upper(user.name),
total: add(order.price, order.tax),
displayName: coalesce(user.displayName, user.name, 'Unknown'),
}))
```

### HIGH: Missing conditional expression helpers

Use `coalesce()` for null/undefined fallbacks and `caseWhen()` for conditional
computed fields. JavaScript operators like `||` or ternaries do not build query
expressions inside standard `.select()` callbacks.

```ts
// WRONG -- document.title is a query ref, not a runtime string
.select(({ document }) => ({
displayTitle: document.title || 'Untitled document',
}))

// CORRECT -- fallback for null/undefined
.select(({ document }) => ({
displayTitle: coalesce(document.title, 'Untitled document'),
}))

// CORRECT -- fallback for null/undefined and empty string
.select(({ document }) => ({
displayTitle: caseWhen(
eq(coalesce(document.title, ''), ''),
'Untitled document',
document.title,
),
}))
```

Use `fn.select()` only when you genuinely need arbitrary JavaScript; it cannot
be optimized like expression-based `.select()`.

### HIGH: .distinct() without .select()

`distinct()` deduplicates by the selected columns. Without `select()`, throws `DistinctRequiresSelectError`.
Expand Down
8 changes: 8 additions & 0 deletions packages/db/skills/db-core/mutations-optimistic/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an ambient
stack. Any `collection.insert/update/delete` call joins the ambient transaction
automatically via `getActiveTransaction()`.

For mutations captured by a manual transaction, collection-level
`onInsert`/`onUpdate`/`onDelete` handlers are not invoked automatically. The
manual transaction's `mutationFn` is responsible for persisting
`transaction.mutations`. This makes `createTransaction({ autoCommit: false })`
a good fit for draft-style flows where local state updates immediately but the
server call waits for Save/Blur; call `tx.rollback()` to discard the optimistic
changes.

### 4. Mutation handler with refetch (QueryCollection pattern)

```ts
Expand Down
Loading