Skip to content

Commit ec59984

Browse files
KyleAMathewsclaude
andauthored
docs(db): clarify caseWhen, coalesce, manual transactions, and multi-endpoint behavior (#1544)
* docs(db): clarify caseWhen, coalesce, manual transactions, and multi-endpoint behavior Address documentation feedback: add caseWhen operator docs, clarify coalesce handles null/undefined, document manual transaction handler bypass semantics, and warn about multi-endpoint Query Collection pitfalls. Fix categorization of utility functions and reference index ordering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add changeset for docs clarifications Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs(db): mention unionAll as option for multi-endpoint collections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs(db): replace redundant manual transaction example with explanatory prose Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6238a2d commit ec59984

8 files changed

Lines changed: 131 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/db': patch
3+
---
4+
5+
Clarify documentation for caseWhen, coalesce, manual transactions, and multi-endpoint Query Collection behavior. Add utility function categorization and fix reference index ordering.

docs/collections/query-collection.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,27 @@ The query collection treats the `queryFn` result as the **complete state** of th
471471
- Items in the query result but not in the collection will be inserted
472472
- Items present in both will be updated if they differ
473473

474+
This is important when the same entity type can be loaded from multiple REST
475+
endpoints. For example, do not point the same Query Collection at
476+
`/api/documents/preview` for one load and `/api/documents/deleted` for another
477+
unless each result represents the complete state for that collection
478+
scope. A narrower endpoint can otherwise remove rows that were loaded from a
479+
different endpoint.
480+
481+
For multiple endpoint or subset-loading use cases, choose the pattern that
482+
matches your API semantics:
483+
484+
- Use `syncMode: 'on-demand'` when one logical collection can serve different
485+
subsets of data. In this mode, query predicates (`where`, `orderBy`, `limit`,
486+
and `offset`) are passed to your `queryFn` via `ctx.meta.loadSubsetOptions`,
487+
letting you translate them into API parameters.
488+
- Use separate Query Collections when endpoints represent distinct server scopes
489+
whose results should not replace each other. Use `unionAll` to combine them
490+
into a single query when you need a unified view across endpoints.
491+
- Use direct writes such as `writeUpsert`/`writeBatch` for lower-level
492+
incremental loading when you intentionally want to merge server responses into
493+
the synced store yourself.
494+
474495
### Empty Array Behavior
475496

476497
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".

docs/guides/live-queries.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2551,12 +2551,57 @@ Add two numbers:
25512551
add(user.salary, user.bonus)
25522552
```
25532553

2554+
### Utility Functions
2555+
25542556
#### `coalesce(...values)`
2555-
Return the first non-null value:
2557+
Return the first non-null/undefined value:
25562558
```ts
25572559
coalesce(user.displayName, user.name, 'Unknown')
25582560
```
25592561

2562+
This is useful for display fallbacks when the stored value is `null` or
2563+
`undefined`:
2564+
2565+
```ts
2566+
.select(({ document }) => ({
2567+
...document,
2568+
displayTitle: coalesce(document.title, 'Untitled document'),
2569+
}))
2570+
```
2571+
2572+
If you also need to treat another value, such as an empty string, as missing,
2573+
use `caseWhen` to express that condition explicitly.
2574+
2575+
#### `caseWhen(condition, value, ...)`
2576+
Return the value for the first matching condition, similar to SQL `CASE WHEN`.
2577+
Arguments are provided as condition/value pairs followed by an optional default
2578+
value:
2579+
2580+
```ts
2581+
caseWhen(gt(user.age, 65), 'senior', gt(user.age, 18), 'adult', 'minor')
2582+
```
2583+
2584+
If no condition matches and no default value is provided, scalar expressions
2585+
return `null`.
2586+
2587+
Use `caseWhen` when a computed field needs conditional logic that cannot be
2588+
represented with `coalesce` alone. For example, to display a fallback for both
2589+
`null`/`undefined` and empty-string titles:
2590+
2591+
```ts
2592+
.select(({ document }) => ({
2593+
...document,
2594+
displayTitle: caseWhen(
2595+
eq(coalesce(document.title, ''), ''),
2596+
'Untitled document',
2597+
document.title,
2598+
),
2599+
}))
2600+
```
2601+
2602+
`caseWhen` can also be used in expression contexts such as `select`, `where`,
2603+
`orderBy`, `groupBy`, `having`, and equality join operands.
2604+
25602605
### Aggregate Functions
25612606

25622607
#### `count(value)`

docs/guides/mutations.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,20 @@ await tx.commit()
788788
tx.rollback()
789789
```
790790

791+
When you call collection methods inside `tx.mutate()`, the mutations are
792+
captured by the manual transaction. The collection's `onInsert`, `onUpdate`, and
793+
`onDelete` handlers are not invoked for those mutations; the
794+
manual transaction's `mutationFn` is responsible for persisting
795+
`transaction.mutations`.
796+
797+
This makes manual transactions a good fit for draft-style workflows where local
798+
state should update immediately, but persistence should wait for a later user
799+
action such as Save or Blur. Each `tx.mutate()` call updates the optimistic
800+
state instantly, so the UI reflects changes as the user types. When the user
801+
triggers Save, `tx.commit()` fires the `mutationFn` to persist all accumulated
802+
mutations in a single batch. If the user cancels instead, `tx.rollback()`
803+
discards the optimistic changes and reverts the UI.
804+
791805
### Multi-Step Workflows
792806

793807
Manual transactions excel at complex workflows:

docs/reference/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ title: "@tanstack/db"
265265
- [add](functions/add.md)
266266
- [and](functions/and.md)
267267
- [avg](functions/avg.md)
268+
- [caseWhen](functions/caseWhen.md)
268269
- [clearQueryPatterns](functions/clearQueryPatterns.md)
269270
- [coalesce](functions/coalesce.md)
270271
- [compileExpression](functions/compileExpression.md)

docs/reference/variables/operators.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ title: operators
66
# Variable: operators
77

88
```ts
9-
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"];
9+
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"];
1010
```
1111

12-
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)
12+
Defined in: [packages/db/src/query/builder/functions.ts](https://github.com/TanStack/db/blob/main/packages/db/src/query/builder/functions.ts)
1313

1414
All supported operator names in TanStack DB expressions

packages/db/skills/db-core/live-queries/SKILL.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ description: >
55
fullJoin, select, fn.select, groupBy, having, orderBy, limit, offset, distinct,
66
findOne. Operators: eq, gt, gte, lt, lte, like, ilike, inArray, isNull,
77
isUndefined, and, or, not. Aggregates: count, sum, avg, min, max. String
8-
functions: upper, lower, length, concat, coalesce. Math: add. $selected
9-
namespace. createLiveQueryCollection. Derived collections. Predicate push-down.
8+
functions: upper, lower, length, concat. Utility: coalesce, caseWhen. Math: add.
9+
$selected namespace. createLiveQueryCollection. Derived collections. Predicate push-down.
1010
Incremental view maintenance via differential dataflow (d2ts). Virtual
1111
properties ($synced, $origin, $key, $collectionId). Includes subqueries
1212
for hierarchical data. toArray and concat(toArray(...)) scalar includes.
@@ -385,7 +385,7 @@ const { data } = useLiveQuery((q) =>
385385

386386
### HIGH: Not using the full operator set
387387

388-
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.
388+
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.
389389

390390
```ts
391391
// WRONG
@@ -398,9 +398,40 @@ The library provides string functions (`upper`, `lower`, `length`, `concat`), ma
398398
.select(({ user, order }) => ({
399399
name: upper(user.name),
400400
total: add(order.price, order.tax),
401+
displayName: coalesce(user.displayName, user.name, 'Unknown'),
401402
}))
402403
```
403404

405+
### HIGH: Missing conditional expression helpers
406+
407+
Use `coalesce()` for null/undefined fallbacks and `caseWhen()` for conditional
408+
computed fields. JavaScript operators like `||` or ternaries do not build query
409+
expressions inside standard `.select()` callbacks.
410+
411+
```ts
412+
// WRONG -- document.title is a query ref, not a runtime string
413+
.select(({ document }) => ({
414+
displayTitle: document.title || 'Untitled document',
415+
}))
416+
417+
// CORRECT -- fallback for null/undefined
418+
.select(({ document }) => ({
419+
displayTitle: coalesce(document.title, 'Untitled document'),
420+
}))
421+
422+
// CORRECT -- fallback for null/undefined and empty string
423+
.select(({ document }) => ({
424+
displayTitle: caseWhen(
425+
eq(coalesce(document.title, ''), ''),
426+
'Untitled document',
427+
document.title,
428+
),
429+
}))
430+
```
431+
432+
Use `fn.select()` only when you genuinely need arbitrary JavaScript; it cannot
433+
be optimized like expression-based `.select()`.
434+
404435
### HIGH: .distinct() without .select()
405436

406437
`distinct()` deduplicates by the selected columns. Without `select()`, throws `DistinctRequiresSelectError`.

packages/db/skills/db-core/mutations-optimistic/SKILL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,14 @@ Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an ambient
209209
stack. Any `collection.insert/update/delete` call joins the ambient transaction
210210
automatically via `getActiveTransaction()`.
211211

212+
For mutations captured by a manual transaction, collection-level
213+
`onInsert`/`onUpdate`/`onDelete` handlers are not invoked automatically. The
214+
manual transaction's `mutationFn` is responsible for persisting
215+
`transaction.mutations`. This makes `createTransaction({ autoCommit: false })`
216+
a good fit for draft-style flows where local state updates immediately but the
217+
server call waits for Save/Blur; call `tx.rollback()` to discard the optimistic
218+
changes.
219+
212220
### 4. Mutation handler with refetch (QueryCollection pattern)
213221

214222
```ts

0 commit comments

Comments
 (0)