Skip to content

Commit e478d53

Browse files
authored
add support for composable queries (#232)
1 parent c639b10 commit e478d53

File tree

18 files changed

+859
-109
lines changed

18 files changed

+859
-109
lines changed

.changeset/wicked-grapes-thank.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@tanstack/react-db": patch
3+
"@tanstack/vue-db": patch
4+
"@tanstack/db": patch
5+
---
6+
7+
add support for composable queries

packages/db/src/query/builder/ref-proxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Ref, Value } from "../ir.js"
1+
import { PropRef, Value } from "../ir.js"
22
import type { BasicExpression } from "../ir.js"
33

44
export interface RefProxy<T = any> {
@@ -124,7 +124,7 @@ export function toExpression<T = any>(value: T): BasicExpression<T>
124124
export function toExpression(value: RefProxy<any>): BasicExpression<any>
125125
export function toExpression(value: any): BasicExpression<any> {
126126
if (isRefProxy(value)) {
127-
return new Ref(value.__path)
127+
return new PropRef(value.__path)
128128
}
129129
// If it's already an Expression (Func, Ref, Value) or Agg, return it directly
130130
if (

packages/db/src/query/builder/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ export type RefProxyFor<T> = OmitRefProxy<
139139
: RefProxy<T>
140140
>
141141

142+
// This is the public type that is exported from the query builder
143+
// and is used when constructing reusable query callbacks.
144+
export type Ref<T> = RefProxyFor<T>
145+
142146
type OmitRefProxy<T> = Omit<T, `__refProxy` | `__path` | `__type`>
143147

144148
// The core RefProxy interface

packages/db/src/query/compiler/evaluators.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BasicExpression, Func, Ref } from "../ir.js"
1+
import type { BasicExpression, Func, PropRef } from "../ir.js"
22
import type { NamespacedRow } from "../../types.js"
33

44
/**
@@ -36,7 +36,7 @@ export function compileExpression(expr: BasicExpression): CompiledExpression {
3636
/**
3737
* Compiles a reference expression into an optimized evaluator
3838
*/
39-
function compileRef(ref: Ref): CompiledExpression {
39+
function compileRef(ref: PropRef): CompiledExpression {
4040
const [tableAlias, ...propertyPath] = ref.path
4141

4242
if (!tableAlias) {

packages/db/src/query/compiler/group-by.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { filter, groupBy, groupByOperators, map } from "@electric-sql/d2mini"
2-
import { Func, Ref } from "../ir.js"
2+
import { Func, PropRef } from "../ir.js"
33
import { compileExpression } from "./evaluators.js"
44
import type {
55
Aggregate,
@@ -372,7 +372,7 @@ function transformHavingClause(
372372
for (const [alias, selectExpr] of Object.entries(selectClause)) {
373373
if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {
374374
// Replace with a reference to the computed aggregate
375-
return new Ref([`result`, alias])
375+
return new PropRef([`result`, alias])
376376
}
377377
}
378378
// If no matching aggregate found in SELECT, throw error
@@ -398,7 +398,7 @@ function transformHavingClause(
398398
const alias = refExpr.path[0]!
399399
if (selectClause[alias]) {
400400
// This is a reference to a SELECT alias, convert to result.alias
401-
return new Ref([`result`, alias])
401+
return new PropRef([`result`, alias])
402402
}
403403
}
404404
// Return as-is for other refs

packages/db/src/query/index.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,7 @@ export {
4141
} from "./builder/functions.js"
4242

4343
// Ref proxy utilities
44-
export { val, toExpression, isRefProxy } from "./builder/ref-proxy.js"
45-
46-
// IR types (for advanced usage)
47-
export type {
48-
QueryIR,
49-
BasicExpression as Expression,
50-
Aggregate,
51-
CollectionRef,
52-
QueryRef,
53-
JoinClause,
54-
} from "./ir.js"
44+
export type { Ref } from "./builder/types.js"
5545

5646
// Compiler
5747
export { compileQuery } from "./compiler/index.js"

packages/db/src/query/ir.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export class QueryRef extends BaseExpression {
8484
}
8585
}
8686

87-
export class Ref<T = any> extends BaseExpression<T> {
87+
export class PropRef<T = any> extends BaseExpression<T> {
8888
public type = `ref` as const
8989
constructor(
9090
public path: Array<string> // path to the property in the collection, with the alias as the first element
@@ -115,7 +115,7 @@ export class Func<T = any> extends BaseExpression<T> {
115115
// This is the basic expression type that is used in the majority of expression
116116
// builder callbacks (select, where, groupBy, having, orderBy, etc.)
117117
// it doesn't include aggregate functions as those are only used in the select clause
118-
export type BasicExpression<T = any> = Ref<T> | Value<T> | Func<T>
118+
export type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>
119119

120120
export class Aggregate<T = any> extends BaseExpression<T> {
121121
public type = `agg` as const

packages/db/src/query/live-query-collection.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { D2, MultiSet, output } from "@electric-sql/d2mini"
22
import { createCollection } from "../collection.js"
33
import { compileQuery } from "./compiler/index.js"
4-
import { buildQuery } from "./builder/index.js"
4+
import { buildQuery, getQueryIR } from "./builder/index.js"
55
import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
66
import type { Collection } from "../collection.js"
77
import type {
@@ -55,7 +55,9 @@ export interface LiveQueryCollectionConfig<
5555
/**
5656
* Query builder function that defines the live query
5757
*/
58-
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
58+
query:
59+
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
60+
| QueryBuilder<TContext>
5961

6062
/**
6163
* Function to extract the key from result items
@@ -119,8 +121,11 @@ export function liveQueryCollectionOptions<
119121
// Generate a unique ID if not provided
120122
const id = config.id || `live-query-${++liveQueryCollectionCounter}`
121123

122-
// Build the query using the provided query builder function
123-
const query = buildQuery<TContext>(config.query)
124+
// Build the query using the provided query builder function or instance
125+
const query =
126+
typeof config.query === `function`
127+
? buildQuery<TContext>(config.query)
128+
: getQueryIR(config.query)
124129

125130
// WeakMap to store the keys of the results so that we can retreve them in the
126131
// getKey function
@@ -401,11 +406,11 @@ export function createLiveQueryCollection<
401406
if (typeof configOrQuery === `function`) {
402407
// Simple query function case
403408
const config: LiveQueryCollectionConfig<TContext, TResult> = {
404-
query: configOrQuery,
409+
query: configOrQuery as (
410+
q: InitialQueryBuilder
411+
) => QueryBuilder<TContext>,
405412
}
406413
const options = liveQueryCollectionOptions<TContext, TResult>(config)
407-
408-
// Use a bridge function that handles the type compatibility cleanly
409414
return bridgeToCreateCollection(options)
410415
} else {
411416
// Config object case
@@ -414,8 +419,6 @@ export function createLiveQueryCollection<
414419
TResult
415420
> & { utils?: TUtils }
416421
const options = liveQueryCollectionOptions<TContext, TResult>(config)
417-
418-
// Use a bridge function that handles the type compatibility cleanly
419422
return bridgeToCreateCollection({
420423
...options,
421424
utils: config.utils,

packages/db/tests/query/builder/ref-proxy.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
toExpression,
66
val,
77
} from "../../../src/query/builder/ref-proxy.js"
8-
import { Ref, Value } from "../../../src/query/ir.js"
8+
import { PropRef, Value } from "../../../src/query/ir.js"
99

1010
describe(`ref-proxy`, () => {
1111
describe(`createRefProxy`, () => {
@@ -170,9 +170,9 @@ describe(`ref-proxy`, () => {
170170
const userIdProxy = proxy.users.id
171171

172172
const expr = toExpression(userIdProxy)
173-
expect(expr).toBeInstanceOf(Ref)
173+
expect(expr).toBeInstanceOf(PropRef)
174174
expect(expr.type).toBe(`ref`)
175-
expect((expr as Ref).path).toEqual([`users`, `id`])
175+
expect((expr as PropRef).path).toEqual([`users`, `id`])
176176
})
177177

178178
it(`converts literal values to Value expression`, () => {
@@ -183,7 +183,7 @@ describe(`ref-proxy`, () => {
183183
})
184184

185185
it(`returns existing expressions unchanged`, () => {
186-
const refExpr = new Ref([`users`, `id`])
186+
const refExpr = new PropRef([`users`, `id`])
187187
const valExpr = new Value(42)
188188

189189
expect(toExpression(refExpr)).toBe(refExpr)

packages/db/tests/query/compiler/basic.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, test } from "vitest"
22
import { D2, MultiSet, output } from "@electric-sql/d2mini"
33
import { compileQuery } from "../../../src/query/compiler/index.js"
4-
import { CollectionRef, Func, Ref, Value } from "../../../src/query/ir.js"
4+
import { CollectionRef, Func, PropRef, Value } from "../../../src/query/ir.js"
55
import type { QueryIR } from "../../../src/query/ir.js"
66
import type { CollectionImpl } from "../../../src/collection.js"
77

@@ -82,9 +82,9 @@ describe(`Query2 Compiler`, () => {
8282
const query: QueryIR = {
8383
from: new CollectionRef(usersCollection, `users`),
8484
select: {
85-
id: new Ref([`users`, `id`]),
86-
name: new Ref([`users`, `name`]),
87-
age: new Ref([`users`, `age`]),
85+
id: new PropRef([`users`, `id`]),
86+
name: new PropRef([`users`, `name`]),
87+
age: new PropRef([`users`, `age`]),
8888
},
8989
}
9090

@@ -150,11 +150,11 @@ describe(`Query2 Compiler`, () => {
150150
const query: QueryIR = {
151151
from: new CollectionRef(usersCollection, `users`),
152152
select: {
153-
id: new Ref([`users`, `id`]),
154-
name: new Ref([`users`, `name`]),
155-
age: new Ref([`users`, `age`]),
153+
id: new PropRef([`users`, `id`]),
154+
name: new PropRef([`users`, `name`]),
155+
age: new PropRef([`users`, `age`]),
156156
},
157-
where: [new Func(`gt`, [new Ref([`users`, `age`]), new Value(20)])],
157+
where: [new Func(`gt`, [new PropRef([`users`, `age`]), new Value(20)])],
158158
}
159159

160160
const graph = new D2()
@@ -203,13 +203,13 @@ describe(`Query2 Compiler`, () => {
203203
const query: QueryIR = {
204204
from: new CollectionRef(usersCollection, `users`),
205205
select: {
206-
id: new Ref([`users`, `id`]),
207-
name: new Ref([`users`, `name`]),
206+
id: new PropRef([`users`, `id`]),
207+
name: new PropRef([`users`, `name`]),
208208
},
209209
where: [
210210
new Func(`and`, [
211-
new Func(`gt`, [new Ref([`users`, `age`]), new Value(20)]),
212-
new Func(`eq`, [new Ref([`users`, `active`]), new Value(true)]),
211+
new Func(`gt`, [new PropRef([`users`, `age`]), new Value(20)]),
212+
new Func(`eq`, [new PropRef([`users`, `active`]), new Value(true)]),
213213
]),
214214
],
215215
}

0 commit comments

Comments
 (0)