Skip to content

Commit 41c0ea2

Browse files
samwilliscursoragentautofix-ci[bot]
authored
Query once API implementation (#1211)
* feat(db): add queryOnce API for one-shot query execution Implements the queryOnce function as described in the RFC. This provides a lightweight wrapper around createLiveQueryCollection that: - Creates a live query collection with gcTime: 0 - Preloads the data - Extracts results as an array (or single item for findOne) - Automatically cleans up the collection The queryOnce function: - Accepts either a query function or config object - Supports all query builder operations (where, select, join, orderBy, etc.) - Properly handles findOne() queries returning single results - Ensures cleanup happens even on error via try/finally Use cases: - AI/LLM context building - Data export - Background processing - Testing API: ```typescript // Simple query const users = await queryOnce((q) => q.from({ user: usersCollection }) .where(({ user }) => eq(user.active, true)) ) // Single result with findOne const user = await queryOnce((q) => q.from({ user: usersCollection }) .where(({ user }) => eq(user.id, 1)) .findOne() ) // Config object form const orders = await queryOnce({ query: (q) => q.from({ order: ordersCollection }).limit(100) }) ``` * ci: apply automated fixes * fix(db): use gcTime 1 for queryOnce cleanup * docs: add queryOnce to live queries guide * feat(db): allow queryOnce to accept QueryBuilder Co-authored-by: sam.willis <[email protected]> * refactor(db): tighten queryOnce runtime behavior * ci: apply automated fixes * fix(db): normalize queryOnce query builder input * chore: add changeset for queryOnce * ci: apply automated fixes * fix(db): remove redundant queryOnce undefined fallback Drop the unnecessary nullish-coalescing in queryOnce so the single-result path returns the iterator value directly while preserving the existing undefined-on-empty behavior. Made-with: Cursor --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent a55e2bf commit 41c0ea2

5 files changed

Lines changed: 520 additions & 0 deletions

File tree

.changeset/query-once-api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/db': minor
3+
---
4+
5+
Add `queryOnce` helper for one-shot query execution, including `findOne()` support and optional QueryBuilder configs.

docs/guides/live-queries.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The result types are automatically inferred from your query structure, providing
3232
## Table of Contents
3333

3434
- [Creating Live Query Collections](#creating-live-query-collections)
35+
- [One-shot Queries with queryOnce](#one-shot-queries-with-queryonce)
3536
- [From Clause](#from-clause)
3637
- [Where Clauses](#where-clauses)
3738
- [Select Projections](#select)
@@ -114,6 +115,36 @@ const activeUsers = createLiveQueryCollection((q) =>
114115
)
115116
```
116117

118+
## One-shot Queries with queryOnce
119+
120+
If you need a one-time snapshot (no ongoing reactivity), use `queryOnce`. It
121+
creates a live query collection, preloads it, extracts the results, and cleans
122+
up automatically so you do not have to remember to call `cleanup()`.
123+
124+
```ts
125+
import { eq, queryOnce } from '@tanstack/db'
126+
127+
// Basic one-shot query
128+
const activeUsers = await queryOnce((q) =>
129+
q
130+
.from({ user: usersCollection })
131+
.where(({ user }) => eq(user.active, true))
132+
.select(({ user }) => ({ id: user.id, name: user.name }))
133+
)
134+
135+
// Single result with findOne()
136+
const user = await queryOnce((q) =>
137+
q
138+
.from({ user: usersCollection })
139+
.where(({ user }) => eq(user.id, userId))
140+
.findOne()
141+
)
142+
```
143+
144+
Use `queryOnce` for scripts, background tasks, data export, or AI/LLM context
145+
building. `findOne()` resolves to `undefined` when no rows match. For UI
146+
bindings and reactive updates, use live queries instead.
147+
117148
### Using with Frameworks
118149

119150
In React, you can use the `useLiveQuery` hook:

packages/db/src/query/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ export {
7474
liveQueryCollectionOptions,
7575
} from './live-query-collection.js'
7676

77+
// One-shot query execution
78+
export { queryOnce, type QueryOnceConfig } from './query-once.js'
79+
7780
export { type LiveQueryCollectionConfig } from './live/types.js'
7881
export { type LiveQueryCollectionUtils } from './live/collection-config-builder.js'
7982

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createLiveQueryCollection } from './live-query-collection.js'
2+
import type { InitialQueryBuilder, QueryBuilder } from './builder/index.js'
3+
import type { Context, InferResultType } from './builder/types.js'
4+
5+
/**
6+
* Configuration options for queryOnce
7+
*/
8+
export interface QueryOnceConfig<TContext extends Context> {
9+
/**
10+
* Query builder function that defines the query
11+
*/
12+
query:
13+
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
14+
| QueryBuilder<TContext>
15+
// Future: timeout, signal, etc.
16+
}
17+
18+
// Overload 1: Simple query function returning array (non-single result)
19+
/**
20+
* Executes a one-shot query and returns the results as an array.
21+
*
22+
* This function creates a live query collection, preloads it, extracts the results,
23+
* and automatically cleans up the collection. It's ideal for:
24+
* - AI/LLM context building
25+
* - Data export
26+
* - Background processing
27+
* - Testing
28+
*
29+
* @param queryFn - A function that receives the query builder and returns a query
30+
* @returns A promise that resolves to an array of query results
31+
*
32+
* @example
33+
* ```typescript
34+
* // Basic query
35+
* const users = await queryOnce((q) =>
36+
* q.from({ user: usersCollection })
37+
* )
38+
*
39+
* // With filtering and projection
40+
* const activeUserNames = await queryOnce((q) =>
41+
* q.from({ user: usersCollection })
42+
* .where(({ user }) => eq(user.active, true))
43+
* .select(({ user }) => ({ name: user.name }))
44+
* )
45+
* ```
46+
*/
47+
export function queryOnce<TContext extends Context>(
48+
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
49+
): Promise<InferResultType<TContext>>
50+
51+
// Overload 2: Config object form returning array (non-single result)
52+
/**
53+
* Executes a one-shot query using a configuration object.
54+
*
55+
* @param config - Configuration object with the query function
56+
* @returns A promise that resolves to an array of query results
57+
*
58+
* @example
59+
* ```typescript
60+
* const recentOrders = await queryOnce({
61+
* query: (q) =>
62+
* q.from({ order: ordersCollection })
63+
* .orderBy(({ order }) => desc(order.createdAt))
64+
* .limit(100),
65+
* })
66+
* ```
67+
*/
68+
export function queryOnce<TContext extends Context>(
69+
config: QueryOnceConfig<TContext>,
70+
): Promise<InferResultType<TContext>>
71+
72+
// Implementation
73+
export async function queryOnce<TContext extends Context>(
74+
configOrQuery:
75+
| QueryOnceConfig<TContext>
76+
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>),
77+
): Promise<InferResultType<TContext>> {
78+
// Normalize input
79+
const config: QueryOnceConfig<TContext> =
80+
typeof configOrQuery === `function`
81+
? { query: configOrQuery }
82+
: configOrQuery
83+
84+
const query = (q: InitialQueryBuilder) => {
85+
const queryConfig = config.query
86+
return typeof queryConfig === `function` ? queryConfig(q) : queryConfig
87+
}
88+
89+
// Create collection with minimal GC time; preload handles sync start
90+
const collection = createLiveQueryCollection({
91+
query,
92+
gcTime: 1, // Cleanup in next tick when no subscribers (0 disables GC)
93+
})
94+
95+
try {
96+
// Wait for initial data load
97+
await collection.preload()
98+
99+
// Check if this is a single-result query (findOne was called)
100+
const isSingleResult =
101+
(collection.config as { singleResult?: boolean }).singleResult === true
102+
103+
// Extract and return results
104+
if (isSingleResult) {
105+
const first = collection.values().next().value as
106+
| InferResultType<TContext>
107+
| undefined
108+
return first as InferResultType<TContext>
109+
}
110+
return collection.toArray as InferResultType<TContext>
111+
} finally {
112+
// Always cleanup, even on error
113+
await collection.cleanup()
114+
}
115+
}

0 commit comments

Comments
 (0)