Skip to content

Commit 57b0eeb

Browse files
committed
refactor(db): embed evaluator factories directly in IR nodes
Replace global registry pattern with embedded factories for true tree-shaking: - Func nodes now carry their evaluator factory directly - Aggregate nodes now carry their config (factory + valueTransform) directly - Remove registry.ts and aggregate-registry.ts files - Update all operators to pass factory as 3rd argument to Func - Update all aggregates to pass config as 3rd argument to Aggregate - Update internal code (optimizer, predicate-utils, expressions) to preserve factories when transforming Func nodes - Add array overloads to and() and or() for internal usage - Update tests to use builder functions instead of creating IR directly This design eliminates the need for side-effect imports and ensures only imported operators/aggregates are bundled.
1 parent ce195cb commit 57b0eeb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+785
-966
lines changed

.changeset/auto-register-operators.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,53 @@
22
"@tanstack/db": patch
33
---
44

5-
Add auto-registering operators and aggregates for tree-shaking support and custom extensibility.
5+
Refactor operators and aggregates to embed their evaluators directly in IR nodes for true tree-shaking support and custom extensibility.
66

7-
Each operator and aggregate now bundles its builder function and evaluator in a single file, registering itself when imported. This enables:
7+
Each operator and aggregate now bundles its builder function and evaluator factory in a single file. The factory is embedded directly in the `Func` or `Aggregate` IR node, eliminating the need for a global registry. This enables:
88

9-
- **Tree-shaking**: Only operators/aggregates you import are included in your bundle
10-
- **Custom operators**: Use `registerOperator()` to add your own operators
11-
- **Custom aggregates**: Use `registerAggregate()` to add your own aggregate functions
9+
- **True tree-shaking**: Only operators/aggregates you import are included in your bundle
10+
- **No global registry**: No side-effect imports needed; each node is self-contained
11+
- **Custom operators**: Create custom operators by building `Func` nodes with a factory
12+
- **Custom aggregates**: Create custom aggregates by building `Aggregate` nodes with a config
1213

1314
**Custom Operator Example:**
1415

1516
```typescript
16-
import { registerOperator, type EvaluatorFactory } from "@tanstack/db"
17+
import {
18+
Func,
19+
type EvaluatorFactory,
20+
type CompiledExpression,
21+
} from "@tanstack/db"
22+
import { toExpression } from "@tanstack/db/query"
1723

18-
registerOperator("between", (compiledArgs, _isSingleRow) => {
24+
const betweenFactory: EvaluatorFactory = (compiledArgs, _isSingleRow) => {
1925
const [valueEval, minEval, maxEval] = compiledArgs
2026
return (data) => {
2127
const value = valueEval!(data)
2228
return value >= minEval!(data) && value <= maxEval!(data)
2329
}
24-
})
30+
}
31+
32+
function between(value: any, min: any, max: any) {
33+
return new Func(
34+
"between",
35+
[toExpression(value), toExpression(min), toExpression(max)],
36+
betweenFactory
37+
)
38+
}
2539
```
2640

2741
**Custom Aggregate Example:**
2842

2943
```typescript
30-
import { registerAggregate, type ValueExtractor } from "@tanstack/db"
44+
import {
45+
Aggregate,
46+
type AggregateConfig,
47+
type ValueExtractor,
48+
} from "@tanstack/db"
49+
import { toExpression } from "@tanstack/db/query"
3150

32-
// Custom "product" aggregate that multiplies values
33-
registerAggregate("product", {
51+
const productConfig: AggregateConfig = {
3452
factory: (valueExtractor: ValueExtractor) => ({
3553
preMap: valueExtractor,
3654
reduce: (values) => {
@@ -42,5 +60,9 @@ registerAggregate("product", {
4260
},
4361
}),
4462
valueTransform: "numeric",
45-
})
63+
}
64+
65+
function product<T>(arg: T): Aggregate<number> {
66+
return new Aggregate("product", [toExpression(arg)], productConfig)
67+
}
4668
```
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { groupByOperators } from "@tanstack/db-ivm"
22
import { Aggregate } from "../../ir.js"
33
import { toExpression } from "../ref-proxy.js"
4-
import { registerAggregate } from "../../compiler/aggregate-registry.js"
54
import type { AggregateReturnType, ExpressionLike } from "../operators/types.js"
65

76
// ============================================================
8-
// BUILDER FUNCTION
7+
// CONFIG
98
// ============================================================
109

11-
export function avg<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
12-
return new Aggregate(`avg`, [toExpression(arg)]) as AggregateReturnType<T>
10+
const avgConfig = {
11+
factory: groupByOperators.avg,
12+
valueTransform: `numeric` as const,
1313
}
1414

1515
// ============================================================
16-
// AUTO-REGISTRATION
16+
// BUILDER FUNCTION
1717
// ============================================================
1818

19-
registerAggregate(`avg`, {
20-
factory: groupByOperators.avg,
21-
valueTransform: `numeric`,
22-
})
19+
export function avg<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
20+
return new Aggregate(
21+
`avg`,
22+
[toExpression(arg)],
23+
avgConfig
24+
) as AggregateReturnType<T>
25+
}
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import { groupByOperators } from "@tanstack/db-ivm"
22
import { Aggregate } from "../../ir.js"
33
import { toExpression } from "../ref-proxy.js"
4-
import { registerAggregate } from "../../compiler/aggregate-registry.js"
54
import type { ExpressionLike } from "../operators/types.js"
65

76
// ============================================================
8-
// BUILDER FUNCTION
7+
// CONFIG
98
// ============================================================
109

11-
export function count(arg: ExpressionLike): Aggregate<number> {
12-
return new Aggregate(`count`, [toExpression(arg)])
10+
const countConfig = {
11+
factory: groupByOperators.count,
12+
valueTransform: `raw` as const,
1313
}
1414

1515
// ============================================================
16-
// AUTO-REGISTRATION
16+
// BUILDER FUNCTION
1717
// ============================================================
1818

19-
registerAggregate(`count`, {
20-
factory: groupByOperators.count,
21-
valueTransform: `raw`,
22-
})
19+
export function count(arg: ExpressionLike): Aggregate<number> {
20+
return new Aggregate(`count`, [toExpression(arg)], countConfig)
21+
}
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { groupByOperators } from "@tanstack/db-ivm"
22
import { Aggregate } from "../../ir.js"
33
import { toExpression } from "../ref-proxy.js"
4-
import { registerAggregate } from "../../compiler/aggregate-registry.js"
54
import type { AggregateReturnType, ExpressionLike } from "../operators/types.js"
65

76
// ============================================================
8-
// BUILDER FUNCTION
7+
// CONFIG
98
// ============================================================
109

11-
export function max<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
12-
return new Aggregate(`max`, [toExpression(arg)]) as AggregateReturnType<T>
10+
const maxConfig = {
11+
factory: groupByOperators.max,
12+
valueTransform: `numericOrDate` as const,
1313
}
1414

1515
// ============================================================
16-
// AUTO-REGISTRATION
16+
// BUILDER FUNCTION
1717
// ============================================================
1818

19-
registerAggregate(`max`, {
20-
factory: groupByOperators.max,
21-
valueTransform: `numericOrDate`,
22-
})
19+
export function max<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
20+
return new Aggregate(
21+
`max`,
22+
[toExpression(arg)],
23+
maxConfig
24+
) as AggregateReturnType<T>
25+
}
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { groupByOperators } from "@tanstack/db-ivm"
22
import { Aggregate } from "../../ir.js"
33
import { toExpression } from "../ref-proxy.js"
4-
import { registerAggregate } from "../../compiler/aggregate-registry.js"
54
import type { AggregateReturnType, ExpressionLike } from "../operators/types.js"
65

76
// ============================================================
8-
// BUILDER FUNCTION
7+
// CONFIG
98
// ============================================================
109

11-
export function min<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
12-
return new Aggregate(`min`, [toExpression(arg)]) as AggregateReturnType<T>
10+
const minConfig = {
11+
factory: groupByOperators.min,
12+
valueTransform: `numericOrDate` as const,
1313
}
1414

1515
// ============================================================
16-
// AUTO-REGISTRATION
16+
// BUILDER FUNCTION
1717
// ============================================================
1818

19-
registerAggregate(`min`, {
20-
factory: groupByOperators.min,
21-
valueTransform: `numericOrDate`,
22-
})
19+
export function min<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
20+
return new Aggregate(
21+
`min`,
22+
[toExpression(arg)],
23+
minConfig
24+
) as AggregateReturnType<T>
25+
}
Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { groupByOperators } from "@tanstack/db-ivm"
22
import { Aggregate } from "../../ir.js"
33
import { toExpression } from "../ref-proxy.js"
4-
import { registerAggregate } from "../../compiler/aggregate-registry.js"
54
import type { AggregateReturnType, ExpressionLike } from "../operators/types.js"
65

76
// ============================================================
8-
// BUILDER FUNCTION
7+
// CONFIG
98
// ============================================================
109

11-
export function sum<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
12-
return new Aggregate(`sum`, [toExpression(arg)]) as AggregateReturnType<T>
10+
const sumConfig = {
11+
factory: groupByOperators.sum,
12+
valueTransform: `numeric` as const,
1313
}
1414

1515
// ============================================================
16-
// AUTO-REGISTRATION
16+
// BUILDER FUNCTION
1717
// ============================================================
1818

19-
registerAggregate(`sum`, {
20-
factory: groupByOperators.sum,
21-
valueTransform: `numeric`,
22-
})
19+
export function sum<T extends ExpressionLike>(arg: T): AggregateReturnType<T> {
20+
return new Aggregate(
21+
`sum`,
22+
[toExpression(arg)],
23+
sumConfig
24+
) as AggregateReturnType<T>
25+
}
Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,8 @@
11
import { Func } from "../../ir.js"
22
import { toExpression } from "../ref-proxy.js"
3-
import { registerOperator } from "../../compiler/registry.js"
4-
import type { CompiledExpression } from "../../compiler/registry.js"
3+
import type { CompiledExpression } from "../../ir.js"
54
import type { BinaryNumericReturnType, ExpressionLike } from "./types.js"
65

7-
// ============================================================
8-
// BUILDER FUNCTION
9-
// ============================================================
10-
11-
export function add<T1 extends ExpressionLike, T2 extends ExpressionLike>(
12-
left: T1,
13-
right: T2
14-
): BinaryNumericReturnType<T1, T2> {
15-
return new Func(`add`, [
16-
toExpression(left),
17-
toExpression(right),
18-
]) as BinaryNumericReturnType<T1, T2>
19-
}
20-
216
// ============================================================
227
// EVALUATOR
238
// ============================================================
@@ -37,7 +22,16 @@ function addEvaluatorFactory(
3722
}
3823

3924
// ============================================================
40-
// AUTO-REGISTRATION
25+
// BUILDER FUNCTION
4126
// ============================================================
4227

43-
registerOperator(`add`, addEvaluatorFactory)
28+
export function add<T1 extends ExpressionLike, T2 extends ExpressionLike>(
29+
left: T1,
30+
right: T2
31+
): BinaryNumericReturnType<T1, T2> {
32+
return new Func(
33+
`add`,
34+
[toExpression(left), toExpression(right)],
35+
addEvaluatorFactory
36+
) as BinaryNumericReturnType<T1, T2>
37+
}

packages/db/src/query/builder/operators/and.ts

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { Func } from "../../ir.js"
22
import { toExpression } from "../ref-proxy.js"
3-
import { registerOperator } from "../../compiler/registry.js"
4-
import type { BasicExpression } from "../../ir.js"
5-
import type { CompiledExpression } from "../../compiler/registry.js"
3+
import type { BasicExpression, CompiledExpression } from "../../ir.js"
64

75
// ============================================================
86
// TYPES
@@ -11,32 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js"
119
// Helper type for any expression-like value
1210
type ExpressionLike = BasicExpression | any
1311

14-
// ============================================================
15-
// BUILDER FUNCTION
16-
// ============================================================
17-
18-
// Overloads for and() - support 2 or more arguments
19-
export function and(
20-
left: ExpressionLike,
21-
right: ExpressionLike
22-
): BasicExpression<boolean>
23-
export function and(
24-
left: ExpressionLike,
25-
right: ExpressionLike,
26-
...rest: Array<ExpressionLike>
27-
): BasicExpression<boolean>
28-
export function and(
29-
left: ExpressionLike,
30-
right: ExpressionLike,
31-
...rest: Array<ExpressionLike>
32-
): BasicExpression<boolean> {
33-
const allArgs = [left, right, ...rest]
34-
return new Func(
35-
`and`,
36-
allArgs.map((arg) => toExpression(arg))
37-
)
38-
}
39-
4012
// ============================================================
4113
// EVALUATOR
4214
// ============================================================
@@ -77,7 +49,38 @@ function andEvaluatorFactory(
7749
}
7850

7951
// ============================================================
80-
// AUTO-REGISTRATION
52+
// BUILDER FUNCTION
8153
// ============================================================
8254

83-
registerOperator(`and`, andEvaluatorFactory)
55+
// Overloads for and() - support 2 or more arguments, or an array
56+
export function and(
57+
left: ExpressionLike,
58+
right: ExpressionLike
59+
): BasicExpression<boolean>
60+
export function and(
61+
left: ExpressionLike,
62+
right: ExpressionLike,
63+
...rest: Array<ExpressionLike>
64+
): BasicExpression<boolean>
65+
export function and(args: Array<ExpressionLike>): BasicExpression<boolean>
66+
export function and(
67+
leftOrArgs: ExpressionLike | Array<ExpressionLike>,
68+
right?: ExpressionLike,
69+
...rest: Array<ExpressionLike>
70+
): BasicExpression<boolean> {
71+
// Handle array overload
72+
if (Array.isArray(leftOrArgs) && right === undefined) {
73+
return new Func(
74+
`and`,
75+
leftOrArgs.map((arg) => toExpression(arg)),
76+
andEvaluatorFactory
77+
)
78+
}
79+
// Handle variadic overload
80+
const allArgs = [leftOrArgs, right!, ...rest]
81+
return new Func(
82+
`and`,
83+
allArgs.map((arg) => toExpression(arg)),
84+
andEvaluatorFactory
85+
)
86+
}

0 commit comments

Comments
 (0)