Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
0450cff
starting point...
samwillis Jun 18, 2025
7f6b55a
checkpoint
samwillis Jun 18, 2025
19b8749
checkpoint
samwillis Jun 18, 2025
e28ec03
tests
samwillis Jun 18, 2025
3c7ced9
more
samwillis Jun 18, 2025
7d03505
tidy
samwillis Jun 18, 2025
2211d48
checkpoint
samwillis Jun 19, 2025
5cc2b8a
more
samwillis Jun 19, 2025
d9cbd36
basic test of full build -> compile -> run
samwillis Jun 19, 2025
197e04a
checkpoint
samwillis Jun 19, 2025
85f6519
more
samwillis Jun 20, 2025
006898b
WIP groupby
samwillis Jun 20, 2025
c640cbf
groupby with having
samwillis Jun 21, 2025
f5118c0
fix lint errors
samwillis Jun 21, 2025
33f724e
fix lint warnings
samwillis Jun 21, 2025
e7f9cd5
fix tets
samwillis Jun 21, 2025
119d193
fix test type errors
samwillis Jun 21, 2025
9171963
fix lint
samwillis Jun 22, 2025
b4f0c2f
move tests
samwillis Jun 22, 2025
d6dc39c
fix return type for select
samwillis Jun 22, 2025
ffbc839
fix return type when no select
samwillis Jun 22, 2025
4ae3cba
fix lint errors
samwillis Jun 22, 2025
976bd07
update d2mini
samwillis Jun 22, 2025
f04e87f
Merge branch 'main' into query2
samwillis Jun 22, 2025
d1276bb
type tests
samwillis Jun 22, 2025
8d213dd
test for query with no select
samwillis Jun 22, 2025
b200a43
join tests
samwillis Jun 22, 2025
c76996f
tidy
samwillis Jun 22, 2025
a83e432
fix test
samwillis Jun 22, 2025
fe8e23b
fix join types
samwillis Jun 23, 2025
839a0e9
remove unused fn
samwillis Jun 23, 2025
2339381
move type tests out into test-d files
samwillis Jun 23, 2025
de646c0
tests for the query builder callback enspression builder types and fi…
samwillis Jun 23, 2025
29ab969
test subqueries
samwillis Jun 23, 2025
ea051af
fix types for refs and results in joins
samwillis Jun 23, 2025
2aba326
fix lint
samwillis Jun 23, 2025
7ef09d7
Merge branch 'main' into query2
samwillis Jun 23, 2025
4e51e94
fix after merge
samwillis Jun 23, 2025
2ba4cd6
enable use of spread in a select expression
samwillis Jun 24, 2025
bc85141
subquery compiling caching to dedupe
samwillis Jun 24, 2025
12dceae
simplify funciton sigs
samwillis Jun 24, 2025
b159b3a
compiling rather than evaluation of expressions
samwillis Jun 24, 2025
e011f5c
refactor compiled pipeline to have better structure
samwillis Jun 24, 2025
0c67aee
checkpoint before checking out cursor/implement-prd-proposals-and-run…
samwillis Jun 24, 2025
c7c80d3
Merge branch 'main' into query2
samwillis Jun 24, 2025
353e0fc
refactor orderby
samwillis Jun 24, 2025
4723520
fix tests
samwillis Jun 24, 2025
eb4ed13
remove old query engine
samwillis Jun 24, 2025
dcfc38d
tidy
samwillis Jun 24, 2025
f43ddd7
more tests
samwillis Jun 24, 2025
d58b12f
remove unused utils
samwillis Jun 24, 2025
b175c11
remove files
samwillis Jun 24, 2025
ec5f3c4
Merge branch 'main' into query2
samwillis Jun 26, 2025
460a153
incorporate collection lifecycle into live queries
samwillis Jun 26, 2025
6b7d116
ensure that live queries are not set to ready untill all their source…
samwillis Jun 26, 2025
c701c9a
port over missing change
samwillis Jun 26, 2025
c5dbf89
wip react useLiveQuery
samwillis Jun 26, 2025
87fe4f6
Merge branch 'main' into query2
samwillis Jun 26, 2025
44be0f6
wip tests
samwillis Jun 26, 2025
ffe3929
vue useLiveQuery WIP
samwillis Jun 26, 2025
702b915
tidy
samwillis Jun 26, 2025
deb731f
fix lint warnings
samwillis Jun 26, 2025
761954d
add a failing for for insert-update-delete with orderBy
samwillis Jun 26, 2025
02a6615
add a new rowUpdateMode option, fixes orderby
samwillis Jun 26, 2025
6dccf10
remove store dep
samwillis Jun 26, 2025
ab6835f
react useLiveQuery can be passed a collection
samwillis Jun 27, 2025
5c654ab
passing a collection to the react and vue useLiveQuery done
samwillis Jun 27, 2025
1a9f9ba
fix types
samwillis Jun 27, 2025
a315a5d
declutter type prompt in query builder
samwillis Jun 27, 2025
8e1368d
WIP functional variants of the select, where and having query builder…
samwillis Jun 28, 2025
59b065c
jsdoc for the query builder
samwillis Jun 28, 2025
c200ea8
remove alias in test for isIn
samwillis Jun 28, 2025
5d8f995
fix multiple where/having clauses
samwillis Jun 30, 2025
c485605
rename isIn to inArray
samwillis Jun 30, 2025
6435efa
wip change exmaple to use new query syntax
samwillis Jun 30, 2025
a8b7bdb
Merge branch 'main' into query2
samwillis Jun 30, 2025
04f8e50
fix type bug
samwillis Jun 30, 2025
d5a2afc
wip fixes to example
samwillis Jun 30, 2025
47bf1ff
bump d2mini to latest - fixes multi batch joins
samwillis Jun 30, 2025
029c3ad
ensure that the status is set before sending the batch of messages
samwillis Jun 30, 2025
b2e8e19
batch events from optimistic removal when applying sync
samwillis Jul 1, 2025
0bf575e
fix type errors in demo
samwillis Jul 1, 2025
a45f497
rename derived to optimistic
samwillis Jul 1, 2025
e0f31f1
make react useLiveQuery data and state values lazy
samwillis Jul 1, 2025
502b33e
make vue useLiveQuery more fine grade
samwillis Jul 1, 2025
24bdccd
fix linting
samwillis Jul 1, 2025
55b162d
update overview docs to use new syntax
samwillis Jul 3, 2025
4992c5f
tidy up map like methods
samwillis Jul 3, 2025
1b73361
use useSyncExternalStore for useLiveQuery (#225)
samwillis Jul 6, 2025
45bd564
Merge branch 'main' into query2
samwillis Jul 6, 2025
e125e90
post review changes
samwillis Jul 6, 2025
8b3086a
enable a `new Query.from(...) syntax
samwillis Jul 7, 2025
0487334
add support for composable queries
samwillis Jul 7, 2025
44841e6
Merge branch 'main' into samwillis/composables
samwillis Jul 7, 2025
ff0e9f8
changeset
samwillis Jul 7, 2025
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
7 changes: 7 additions & 0 deletions .changeset/wicked-grapes-thank.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tanstack/react-db": patch
"@tanstack/vue-db": patch
"@tanstack/db": patch
---

add support for composable queries
4 changes: 2 additions & 2 deletions packages/db/src/query/builder/ref-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ref, Value } from "../ir.js"
import { PropRef, Value } from "../ir.js"
import type { BasicExpression } from "../ir.js"

export interface RefProxy<T = any> {
Expand Down Expand Up @@ -124,7 +124,7 @@ export function toExpression<T = any>(value: T): BasicExpression<T>
export function toExpression(value: RefProxy<any>): BasicExpression<any>
export function toExpression(value: any): BasicExpression<any> {
if (isRefProxy(value)) {
return new Ref(value.__path)
return new PropRef(value.__path)
}
// If it's already an Expression (Func, Ref, Value) or Agg, return it directly
if (
Expand Down
4 changes: 4 additions & 0 deletions packages/db/src/query/builder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export type RefProxyFor<T> = OmitRefProxy<
: RefProxy<T>
>

// This is the public type that is exported from the query builder
// and is used when constructing reusable query callbacks.
export type Ref<T> = RefProxyFor<T>

type OmitRefProxy<T> = Omit<T, `__refProxy` | `__path` | `__type`>

// The core RefProxy interface
Expand Down
4 changes: 2 additions & 2 deletions packages/db/src/query/compiler/evaluators.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BasicExpression, Func, Ref } from "../ir.js"
import type { BasicExpression, Func, PropRef } from "../ir.js"
import type { NamespacedRow } from "../../types.js"

/**
Expand Down Expand Up @@ -36,7 +36,7 @@ export function compileExpression(expr: BasicExpression): CompiledExpression {
/**
* Compiles a reference expression into an optimized evaluator
*/
function compileRef(ref: Ref): CompiledExpression {
function compileRef(ref: PropRef): CompiledExpression {
const [tableAlias, ...propertyPath] = ref.path

if (!tableAlias) {
Expand Down
6 changes: 3 additions & 3 deletions packages/db/src/query/compiler/group-by.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { filter, groupBy, groupByOperators, map } from "@electric-sql/d2mini"
import { Func, Ref } from "../ir.js"
import { Func, PropRef } from "../ir.js"
import { compileExpression } from "./evaluators.js"
import type {
Aggregate,
Expand Down Expand Up @@ -372,7 +372,7 @@ function transformHavingClause(
for (const [alias, selectExpr] of Object.entries(selectClause)) {
if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {
// Replace with a reference to the computed aggregate
return new Ref([`result`, alias])
return new PropRef([`result`, alias])
}
}
// If no matching aggregate found in SELECT, throw error
Expand All @@ -398,7 +398,7 @@ function transformHavingClause(
const alias = refExpr.path[0]!
if (selectClause[alias]) {
// This is a reference to a SELECT alias, convert to result.alias
return new Ref([`result`, alias])
return new PropRef([`result`, alias])
}
}
// Return as-is for other refs
Expand Down
12 changes: 1 addition & 11 deletions packages/db/src/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,7 @@ export {
} from "./builder/functions.js"

// Ref proxy utilities
export { val, toExpression, isRefProxy } from "./builder/ref-proxy.js"

// IR types (for advanced usage)
export type {
QueryIR,
BasicExpression as Expression,
Aggregate,
CollectionRef,
QueryRef,
JoinClause,
} from "./ir.js"
export type { Ref } from "./builder/types.js"

// Compiler
export { compileQuery } from "./compiler/index.js"
Expand Down
4 changes: 2 additions & 2 deletions packages/db/src/query/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class QueryRef extends BaseExpression {
}
}

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

export class Aggregate<T = any> extends BaseExpression<T> {
public type = `agg` as const
Expand Down
21 changes: 12 additions & 9 deletions packages/db/src/query/live-query-collection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { D2, MultiSet, output } from "@electric-sql/d2mini"
import { createCollection } from "../collection.js"
import { compileQuery } from "./compiler/index.js"
import { buildQuery } from "./builder/index.js"
import { buildQuery, getQueryIR } from "./builder/index.js"
import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
import type { Collection } from "../collection.js"
import type {
Expand Down Expand Up @@ -55,7 +55,9 @@ export interface LiveQueryCollectionConfig<
/**
* Query builder function that defines the live query
*/
query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
query:
| ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
| QueryBuilder<TContext>

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

// Build the query using the provided query builder function
const query = buildQuery<TContext>(config.query)
// Build the query using the provided query builder function or instance
const query =
typeof config.query === `function`
? buildQuery<TContext>(config.query)
: getQueryIR(config.query)

// WeakMap to store the keys of the results so that we can retreve them in the
// getKey function
Expand Down Expand Up @@ -401,11 +406,11 @@ export function createLiveQueryCollection<
if (typeof configOrQuery === `function`) {
// Simple query function case
const config: LiveQueryCollectionConfig<TContext, TResult> = {
query: configOrQuery,
query: configOrQuery as (
q: InitialQueryBuilder
) => QueryBuilder<TContext>,
}
const options = liveQueryCollectionOptions<TContext, TResult>(config)

// Use a bridge function that handles the type compatibility cleanly
return bridgeToCreateCollection(options)
} else {
// Config object case
Expand All @@ -414,8 +419,6 @@ export function createLiveQueryCollection<
TResult
> & { utils?: TUtils }
const options = liveQueryCollectionOptions<TContext, TResult>(config)

// Use a bridge function that handles the type compatibility cleanly
return bridgeToCreateCollection({
...options,
utils: config.utils,
Expand Down
8 changes: 4 additions & 4 deletions packages/db/tests/query/builder/ref-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
toExpression,
val,
} from "../../../src/query/builder/ref-proxy.js"
import { Ref, Value } from "../../../src/query/ir.js"
import { PropRef, Value } from "../../../src/query/ir.js"

describe(`ref-proxy`, () => {
describe(`createRefProxy`, () => {
Expand Down Expand Up @@ -170,9 +170,9 @@ describe(`ref-proxy`, () => {
const userIdProxy = proxy.users.id

const expr = toExpression(userIdProxy)
expect(expr).toBeInstanceOf(Ref)
expect(expr).toBeInstanceOf(PropRef)
expect(expr.type).toBe(`ref`)
expect((expr as Ref).path).toEqual([`users`, `id`])
expect((expr as PropRef).path).toEqual([`users`, `id`])
})

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

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

expect(toExpression(refExpr)).toBe(refExpr)
Expand Down
24 changes: 12 additions & 12 deletions packages/db/tests/query/compiler/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from "vitest"
import { D2, MultiSet, output } from "@electric-sql/d2mini"
import { compileQuery } from "../../../src/query/compiler/index.js"
import { CollectionRef, Func, Ref, Value } from "../../../src/query/ir.js"
import { CollectionRef, Func, PropRef, Value } from "../../../src/query/ir.js"
import type { QueryIR } from "../../../src/query/ir.js"
import type { CollectionImpl } from "../../../src/collection.js"

Expand Down Expand Up @@ -82,9 +82,9 @@ describe(`Query2 Compiler`, () => {
const query: QueryIR = {
from: new CollectionRef(usersCollection, `users`),
select: {
id: new Ref([`users`, `id`]),
name: new Ref([`users`, `name`]),
age: new Ref([`users`, `age`]),
id: new PropRef([`users`, `id`]),
name: new PropRef([`users`, `name`]),
age: new PropRef([`users`, `age`]),
},
}

Expand Down Expand Up @@ -150,11 +150,11 @@ describe(`Query2 Compiler`, () => {
const query: QueryIR = {
from: new CollectionRef(usersCollection, `users`),
select: {
id: new Ref([`users`, `id`]),
name: new Ref([`users`, `name`]),
age: new Ref([`users`, `age`]),
id: new PropRef([`users`, `id`]),
name: new PropRef([`users`, `name`]),
age: new PropRef([`users`, `age`]),
},
where: [new Func(`gt`, [new Ref([`users`, `age`]), new Value(20)])],
where: [new Func(`gt`, [new PropRef([`users`, `age`]), new Value(20)])],
}

const graph = new D2()
Expand Down Expand Up @@ -203,13 +203,13 @@ describe(`Query2 Compiler`, () => {
const query: QueryIR = {
from: new CollectionRef(usersCollection, `users`),
select: {
id: new Ref([`users`, `id`]),
name: new Ref([`users`, `name`]),
id: new PropRef([`users`, `id`]),
name: new PropRef([`users`, `name`]),
},
where: [
new Func(`and`, [
new Func(`gt`, [new Ref([`users`, `age`]), new Value(20)]),
new Func(`eq`, [new Ref([`users`, `active`]), new Value(true)]),
new Func(`gt`, [new PropRef([`users`, `age`]), new Value(20)]),
new Func(`eq`, [new PropRef([`users`, `active`]), new Value(true)]),
]),
],
}
Expand Down
16 changes: 8 additions & 8 deletions packages/db/tests/query/compiler/evaluators.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"
import { compileExpression } from "../../../src/query/compiler/evaluators.js"
import { Func, Ref, Value } from "../../../src/query/ir.js"
import { Func, PropRef, Value } from "../../../src/query/ir.js"
import type { NamespacedRow } from "../../../src/types.js"

describe(`evaluators`, () => {
Expand All @@ -14,38 +14,38 @@ describe(`evaluators`, () => {

describe(`ref compilation`, () => {
it(`throws error for empty reference path`, () => {
const emptyRef = new Ref([])
const emptyRef = new PropRef([])
expect(() => compileExpression(emptyRef)).toThrow(
`Reference path cannot be empty`
)
})

it(`handles simple table reference`, () => {
const ref = new Ref([`users`])
const ref = new PropRef([`users`])
const compiled = compileExpression(ref)
const row: NamespacedRow = { users: { id: 1, name: `John` } }

expect(compiled(row)).toEqual({ id: 1, name: `John` })
})

it(`handles single property access`, () => {
const ref = new Ref([`users`, `name`])
const ref = new PropRef([`users`, `name`])
const compiled = compileExpression(ref)
const row: NamespacedRow = { users: { id: 1, name: `John` } }

expect(compiled(row)).toBe(`John`)
})

it(`handles single property access with undefined table`, () => {
const ref = new Ref([`users`, `name`])
const ref = new PropRef([`users`, `name`])
const compiled = compileExpression(ref)
const row: NamespacedRow = { users: undefined as any }

expect(compiled(row)).toBeUndefined()
})

it(`handles multiple property navigation`, () => {
const ref = new Ref([`users`, `profile`, `bio`])
const ref = new PropRef([`users`, `profile`, `bio`])
const compiled = compileExpression(ref)
const row: NamespacedRow = {
users: { profile: { bio: `Hello world` } },
Expand All @@ -55,15 +55,15 @@ describe(`evaluators`, () => {
})

it(`handles multiple property navigation with null value`, () => {
const ref = new Ref([`users`, `profile`, `bio`])
const ref = new PropRef([`users`, `profile`, `bio`])
const compiled = compileExpression(ref)
const row: NamespacedRow = { users: { profile: null } }

expect(compiled(row)).toBeNull()
})

it(`handles multiple property navigation with undefined table`, () => {
const ref = new Ref([`users`, `profile`, `bio`])
const ref = new PropRef([`users`, `profile`, `bio`])
const compiled = compileExpression(ref)
const row: NamespacedRow = { users: undefined as any }

Expand Down
Loading