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
6 changes: 6 additions & 0 deletions .changeset/local-query-observation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/db': minor
'@tanstack/react-db': minor
---

Add `autoLoad: false` to live-query configuration and React query hooks to observe available rows without initiating source loading. Queries remain reactive across joins and pagination, become ready independently of provider loading, and have separate identities from queries that load data automatically.
1 change: 1 addition & 0 deletions docs/contributing/oracle-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ that test identifiers must copy production's private data structures.
| D2 Index storage | Complete for bounded prefix grammar | The contract, plain-`Map` multiset model, prefixed and unprefixed value grammar, `Index` driver, and per-addition `get`/`has` check are literate. Compaction, presence tracking, and structural payloads remain outside this owner. |
| Pooled live queries | Complete for bounded eq-filter grammar | The contract, independent `eq` model, live-query Collection second formulation, sync/optimistic/mount/cleanup-restart grammar, observer driver, and per-step refinement check are literate. On-demand and persisted sources, `DbClient`, Suspense, and every clause beyond `eq` conjuncts keep the live-query Collection. |
| Live-query deferred acquisition | Complete for bounded synchronous grammar | The law, independent model, history grammar over source states, shapes, and depth, real-Collection driver, and per-step refinement check are literate. Pooled views and DbClient stream preloads have their own blocks. The grammar also adds a peer consumer starting the source and a source truncate before the first subscriber; pinned blocks cover a subscriber that outlives cleanup, reads that wait for readiness, and release of two independent descriptor sources when one start fails. Asynchronous sources, several live-query consumers, and general failure histories stay open. |
| Live queries with automatic loading disabled | [local evaluation oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-auto-load-oracle.test.ts), [React boundary tests](https://github.com/TanStack/db/blob/main/packages/react-db/tests/live-query-auto-load.test.tsx), [Query Collection ingestion](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/live-query-auto-load.test.ts) | Available-row results and zero acquisition across windows, joins, includes, and lifecycle changes. Covers eager/on-demand sources and explicit index presence/absence. React checks identity isolation, mode changes, and pagination; Query Collection checks direct ingestion without fetching. Excludes remote provider protocols, arbitrary nested plans, and other frameworks. |
| Flat-row change tracking | Complete for bounded flat-row grammar | Flat and proxy trackers and an independent change model run the same generated callbacks. Nested values fall back to the proxy, which its own oracles own. |
| Production error messages | `packages/db/tests/production-error-messages-oracle.test.ts` | Every public error class keeps its development message for frozen sample inputs, and its coded production line shows the JSON of each shown input, even for hostile inputs. Every plain `Error`, `TypeError`, `RangeError`, or `AggregateError` site in `packages/db/src` keeps its frozen development template, has a code, and passes each showable interpolated expression or one of its sub-expressions; the TypeScript checker keeps plain objects, such as rows, out of production lines. A projection of an object, such as `Object.keys(handlers)`, is not required. Every `console` call whose argument holds library text is coded, development-only with its text frozen per guarded region, or a development-only decoration of a value. Text that reaches the console through a parameter, such as `setErrorState(message)`, is coded where it is written; the census cannot follow it. `scripts/test-production-errors.mjs` checks that a production bundle drops the full text and every development-only hint, in ESM and CommonJS. Errors built from a caller's value stay outside this owner. |
| Lazy target path identity | Focused compiler boundary | A same-source union/coalesce witness keeps dotted and nested demand paths distinct during target deduplication. |
Expand Down
82 changes: 82 additions & 0 deletions docs/contributing/oracle-reviews/2026-10-14-local-live-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Live-query automatic-loading review

Reviewed implementation: `df2518b61963034b6873fac25fcb3c5a4a5a0194`.
Independent review found no actionable issues within the scope below.

## Why this design

`autoLoad: false` queries data already in collections and keeps results reactive.
Application-owned fetching is one use case: some remote API requests cannot be
derived from the query IR, so the application constructs the request and ingests
its results. The contract is defined in the architecture's
[Automatic loading](../../../packages/db/src/query/live/ARCHITECTURE.md#automatic-loading)
section.

Suppressing `loadSubset` alone is insufficient because subscriptions can start
source sync, which can fetch independently. The implementation suppresses both
and determines readiness after evaluating available rows, without depending on
source loading. Other consumers can still load and update the shared rows.

Filtering, joins, includes, and pagination use the ordinary query engine.
Subscriptions skip the provider-loading callbacks, and ordered windows evaluate
the available rows. This avoids a separate implementation of query evaluation.

Loading mode is part of query identity, including explicit keys, because queries
that request data and queries that only read available rows have different
readiness behavior. The discriminator sits outside the user-supplied key array
to prevent collisions. The default remains `true`, with unchanged default hashes.

## Evidence and limits

The [core oracle](../../../packages/db/tests/live-query-auto-load-oracle.test.ts)
compares real Collection results with independent array calculations.
[React tests](../../../packages/react-db/tests/live-query-auto-load.test.tsx)
cover loading-mode changes, identity isolation, and pagination. The
[Query Collection integration](../../../packages/query-db-collection/tests/live-query-auto-load.test.ts)
checks application-owned ingestion without invoking the collection fetcher.
The requirement numbers below refer to the [oracle guide](../oracle-tests.md).
Additional review probes passed for callback configurations with legacy
dependencies, DbClient preload isolation under the same explicit key, and a
disabled outer query over an inner live-query Collection.

- ORC-001–003: the proposed contract, authority, model, production path, and
limits are stated. Array filter/join/sort/slice calculations preserve selected
values, duplicates, and order independently of incremental execution.
- ORC-004–005: generated replacements and windows cover eager/on-demand sources,
windows, joins, and includes, with and without explicitly created indexes.
Index presence is asserted. Checks cover subscription, preload, truncate,
restart, readiness, and zero acquisition; pinned cases cover incremental
updates, provider failure, and coexistence with a fetching consumer.
- ORC-006: enabling acquisition in the source subscription makes the replay
below fail at the source-start assertion. Restoring `autoLoad: false` passes.
- ORC-007: all twelve mode/index/shape combinations run fixed and random
campaigns with ten cases each, scaled by the standard multiplier. Seed/path
replay is supported; command-model replay does not apply.
- ORC-008–010: the reference calculation is stateless, so additional model-state
obligations do not apply. Cleanup preserves primary failures, and the reduced
failing input retains the source-start assertion.
- ORC-011–012: no shared-fault hypothesis requiring another relational
formulation is claimed. This record identifies the reviewed implementation
and evidence limits; it does not claim general bug-class closure.
- ORC-013–014: empty/nonempty inputs and zero/nonzero windows distinguish
available-row readiness from loading. The fetching-peer and Query Collection
cases check shared ownership and direct ingestion. Arbitrary nested plans,
remote provider protocols, and other frameworks remain outside this coverage.

## Validation

The automatic-loading oracle passes 27 tests. Focused identity/preload checks
pass 33 core tests; the full React suite passes 410 tests with no type errors.
Scoped lint and formatting pass. A temporary merge with upstream
`3c481f6c0455fbfe82ea94d9cf457cf00b20d3e7` passes 257 core tests and 31 React
tests with no type errors.

The full workspace run stopped on four SQLite CLI runtime timeouts. All four
cases pass in isolation, but the full suite has not completed successfully.
A separate ordered-window timeout also reproduces on unchanged upstream code.

Run the calibrated replay from `packages/db`:

```sh
TANSTACK_DB_ORACLE_SEED=112919 TANSTACK_DB_ORACLE_PATH=0:0:0:0:0 TANSTACK_DB_ORACLE_PROPERTY=live-query-auto-load.on-demand.false.window pnpm exec vitest run --coverage.enabled=false tests/live-query-auto-load-oracle.test.ts
```
51 changes: 51 additions & 0 deletions docs/guides/live-queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,57 @@ Use `queryOnce` for scripts, background tasks, data export, or AI/LLM context
building. `findOne()` resolves to `undefined` when no rows match. For UI
bindings and reactive updates, use live queries instead.

### Controlling automatic loading

Set `autoLoad: false` to query data already in your collections without loading
more data. Results stay reactive, including for joins, includes, and pagination.
The default is `true`.

One use case is application-owned fetching when a remote API request cannot be
derived from the query IR. This example ingests custom API results into a Query
Collection named `tasks`, configured with `syncMode: 'on-demand'` so explicit
startup does not trigger an eager fetch:

```tsx
function useApiTasks(projectId: string) {
const request = useQuery({
queryKey: ['api-tasks', projectId],
queryFn: async () => {
const rows = await fetchProjectTasks(projectId)
tasks.startSyncImmediate()
tasks.utils.writeUpsert(rows)
return rows
},
})
const result = useLiveQuery({
autoLoad: false,
query: (q) =>
q.from({ task: tasks }).where(({ task }) => eq(task.projectId, projectId)),
})
return { ...result, isFetching: request.isFetching, fetchError: request.error }
}
```

The same option works with `createLiveQueryCollection({ query, autoLoad: false })`
and `useLiveInfiniteQuery(query, { pageSize: 20, autoLoad: false })`.
For an existing live-query Collection, configure `autoLoad` when creating it.

`isReady` means the available rows have been evaluated, even if empty. It does
not indicate whether all remote data has been loaded. Source loading errors do
not fail this query; query evaluation errors and explicit source cleanup do.
Infinite queries expand the window over available rows, and `hasNextPage`
describes those rows. Track remote pagination separately in your API fetcher.

Other consumers and explicit source startup can still load shared rows. A source
that is itself a live-query Collection supplies its published rows under its own
loading policy. This option does not disable loading globally.

Queries with `autoLoad: false` have separate identities from queries using
`true` or the default, even with the same explicit `queryKey`. Switching between
these modes replaces the query and resets infinite-query pages. If a callback
returns the configuration and uses a legacy dependency array, include `autoLoad`
in those dependencies.

### Using with Frameworks

In React, you can use the `useLiveQuery` hook:
Expand Down
Loading