From efa675638ebcfaf84cbc91d3bb779f165fc7c691 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Dec 2025 23:18:46 +0000 Subject: [PATCH 1/7] feat(db): implement auto-registering operators for tree-shaking This enables tree-shaking by having each operator register its own evaluator when imported, rather than relying on a monolithic switch statement. Key changes: - Add registry.ts with registerOperator/getOperatorEvaluator APIs - Create individual operator files (eq, gt, gte, lt, lte, and, or, not, in, like, ilike, upper, lower, length, concat, coalesce, add, subtract, multiply, divide, isNull, isUndefined) - Each operator file bundles builder function + evaluator + registration - Modify evaluators.ts to use registry lookup instead of switch - Update query/index.ts to export from new operator modules - Export compileExpressionInternal for operator modules to use The pattern: importing an operator causes its file to execute, which calls registerOperator, adding it to the registry. By query compile time, all operators in use are already registered. --- .../db/src/query/builder/operators/add.ts | 55 +++ .../db/src/query/builder/operators/and.ts | 83 ++++ .../src/query/builder/operators/coalesce.ts | 48 +++ .../db/src/query/builder/operators/concat.ts | 57 +++ .../db/src/query/builder/operators/divide.ts | 56 +++ packages/db/src/query/builder/operators/eq.ts | 77 ++++ packages/db/src/query/builder/operators/gt.ts | 76 ++++ .../db/src/query/builder/operators/gte.ts | 76 ++++ .../db/src/query/builder/operators/ilike.ts | 59 +++ packages/db/src/query/builder/operators/in.ts | 58 +++ .../db/src/query/builder/operators/index.ts | 38 ++ .../db/src/query/builder/operators/isNull.ts | 42 ++ .../query/builder/operators/isUndefined.ts | 42 ++ .../db/src/query/builder/operators/length.ts | 53 +++ .../db/src/query/builder/operators/like.ts | 89 +++++ .../db/src/query/builder/operators/lower.ts | 47 +++ packages/db/src/query/builder/operators/lt.ts | 76 ++++ .../db/src/query/builder/operators/lte.ts | 76 ++++ .../src/query/builder/operators/multiply.ts | 55 +++ .../db/src/query/builder/operators/not.ts | 53 +++ packages/db/src/query/builder/operators/or.ts | 81 ++++ .../src/query/builder/operators/subtract.ts | 55 +++ .../db/src/query/builder/operators/upper.ts | 47 +++ packages/db/src/query/compiler/evaluators.ts | 360 ++---------------- packages/db/src/query/compiler/registry.ts | 52 +++ packages/db/src/query/index.ts | 24 +- 26 files changed, 1499 insertions(+), 336 deletions(-) create mode 100644 packages/db/src/query/builder/operators/add.ts create mode 100644 packages/db/src/query/builder/operators/and.ts create mode 100644 packages/db/src/query/builder/operators/coalesce.ts create mode 100644 packages/db/src/query/builder/operators/concat.ts create mode 100644 packages/db/src/query/builder/operators/divide.ts create mode 100644 packages/db/src/query/builder/operators/eq.ts create mode 100644 packages/db/src/query/builder/operators/gt.ts create mode 100644 packages/db/src/query/builder/operators/gte.ts create mode 100644 packages/db/src/query/builder/operators/ilike.ts create mode 100644 packages/db/src/query/builder/operators/in.ts create mode 100644 packages/db/src/query/builder/operators/index.ts create mode 100644 packages/db/src/query/builder/operators/isNull.ts create mode 100644 packages/db/src/query/builder/operators/isUndefined.ts create mode 100644 packages/db/src/query/builder/operators/length.ts create mode 100644 packages/db/src/query/builder/operators/like.ts create mode 100644 packages/db/src/query/builder/operators/lower.ts create mode 100644 packages/db/src/query/builder/operators/lt.ts create mode 100644 packages/db/src/query/builder/operators/lte.ts create mode 100644 packages/db/src/query/builder/operators/multiply.ts create mode 100644 packages/db/src/query/builder/operators/not.ts create mode 100644 packages/db/src/query/builder/operators/or.ts create mode 100644 packages/db/src/query/builder/operators/subtract.ts create mode 100644 packages/db/src/query/builder/operators/upper.ts create mode 100644 packages/db/src/query/compiler/registry.ts diff --git a/packages/db/src/query/builder/operators/add.ts b/packages/db/src/query/builder/operators/add.ts new file mode 100644 index 000000000..97113d48a --- /dev/null +++ b/packages/db/src/query/builder/operators/add.ts @@ -0,0 +1,55 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for binary numeric operations (combines nullability of both operands) +type BinaryNumericReturnType<_T1, _T2> = BasicExpression< + number | undefined | null +> + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function add( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func(`add`, [ + toExpression(left), + toExpression(right), + ]) as BinaryNumericReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function addEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + return (a ?? 0) + (b ?? 0) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`add`, addEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/and.ts b/packages/db/src/query/builder/operators/and.ts new file mode 100644 index 000000000..5e11eaff3 --- /dev/null +++ b/packages/db/src/query/builder/operators/and.ts @@ -0,0 +1,83 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +// Overloads for and() - support 2 or more arguments +export function and( + left: ExpressionLike, + right: ExpressionLike +): BasicExpression +export function and( + left: ExpressionLike, + right: ExpressionLike, + ...rest: Array +): BasicExpression +export function and( + left: ExpressionLike, + right: ExpressionLike, + ...rest: Array +): BasicExpression { + const allArgs = [left, right, ...rest] + return new Func( + `and`, + allArgs.map((arg) => toExpression(arg)) + ) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function andEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + return (data: any) => { + // 3-valued logic for AND: + // - false AND anything = false (short-circuit) + // - null AND false = false + // - null AND anything (except false) = null + // - anything (except false) AND null = null + // - true AND true = true + let hasUnknown = false + for (const compiledArg of compiledArgs) { + const result = compiledArg(data) + if (result === false) { + return false + } + if (isUnknown(result)) { + hasUnknown = true + } + } + // If we got here, no operand was false + // If any operand was null, return null (UNKNOWN) + if (hasUnknown) { + return null + } + + return true + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`and`, andEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/coalesce.ts b/packages/db/src/query/builder/operators/coalesce.ts new file mode 100644 index 000000000..5e47ae375 --- /dev/null +++ b/packages/db/src/query/builder/operators/coalesce.ts @@ -0,0 +1,48 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function coalesce(...args: Array): BasicExpression { + return new Func( + `coalesce`, + args.map((arg) => toExpression(arg)) + ) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function coalesceEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + return (data: any) => { + for (const evaluator of compiledArgs) { + const value = evaluator(data) + if (value !== null && value !== undefined) { + return value + } + } + return null + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`coalesce`, coalesceEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/concat.ts b/packages/db/src/query/builder/operators/concat.ts new file mode 100644 index 000000000..a3cf8cb26 --- /dev/null +++ b/packages/db/src/query/builder/operators/concat.ts @@ -0,0 +1,57 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function concat( + ...args: Array +): BasicExpression { + return new Func( + `concat`, + args.map((arg) => toExpression(arg)) + ) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function concatEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + return (data: any) => { + return compiledArgs + .map((evaluator) => { + const arg = evaluator(data) + try { + return String(arg ?? ``) + } catch { + try { + return JSON.stringify(arg) || `` + } catch { + return `[object]` + } + } + }) + .join(``) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`concat`, concatEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/divide.ts b/packages/db/src/query/builder/operators/divide.ts new file mode 100644 index 000000000..dfdd0cde9 --- /dev/null +++ b/packages/db/src/query/builder/operators/divide.ts @@ -0,0 +1,56 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for binary numeric operations (combines nullability of both operands) +type BinaryNumericReturnType<_T1, _T2> = BasicExpression< + number | undefined | null +> + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function divide( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func(`divide`, [ + toExpression(left), + toExpression(right), + ]) as BinaryNumericReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function divideEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + const divisor = b ?? 0 + return divisor !== 0 ? (a ?? 0) / divisor : null + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`divide`, divideEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/eq.ts b/packages/db/src/query/builder/operators/eq.ts new file mode 100644 index 000000000..53bc2b212 --- /dev/null +++ b/packages/db/src/query/builder/operators/eq.ts @@ -0,0 +1,77 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import { areValuesEqual, normalizeValue } from "../../../utils/comparison.js" +import type { Aggregate, BasicExpression } from "../../ir.js" +import type { RefProxy } from "../ref-proxy.js" +import type { RefLeaf } from "../types.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type ComparisonOperand = + | RefProxy + | RefLeaf + | T + | BasicExpression + | undefined + | null + +type ComparisonOperandPrimitive = + | T + | BasicExpression + | undefined + | null + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function eq( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function eq( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function eq(left: Aggregate, right: any): BasicExpression +export function eq(left: any, right: any): BasicExpression { + return new Func(`eq`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function eqEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = normalizeValue(argA(data)) + const b = normalizeValue(argB(data)) + + // 3-valued logic: comparison with null/undefined returns UNKNOWN + if (isUnknown(a) || isUnknown(b)) { + return null + } + + return areValuesEqual(a, b) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`eq`, eqEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/gt.ts b/packages/db/src/query/builder/operators/gt.ts new file mode 100644 index 000000000..ca10b283f --- /dev/null +++ b/packages/db/src/query/builder/operators/gt.ts @@ -0,0 +1,76 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { Aggregate, BasicExpression } from "../../ir.js" +import type { RefProxy } from "../ref-proxy.js" +import type { RefLeaf } from "../types.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type ComparisonOperand = + | RefProxy + | RefLeaf + | T + | BasicExpression + | undefined + | null + +type ComparisonOperandPrimitive = + | T + | BasicExpression + | undefined + | null + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function gt( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function gt( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function gt(left: Aggregate, right: any): BasicExpression +export function gt(left: any, right: any): BasicExpression { + return new Func(`gt`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function gtEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + + // In 3-valued logic, any comparison with null/undefined returns UNKNOWN + if (isUnknown(a) || isUnknown(b)) { + return null + } + + return a > b + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`gt`, gtEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/gte.ts b/packages/db/src/query/builder/operators/gte.ts new file mode 100644 index 000000000..fb0b38c62 --- /dev/null +++ b/packages/db/src/query/builder/operators/gte.ts @@ -0,0 +1,76 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { Aggregate, BasicExpression } from "../../ir.js" +import type { RefProxy } from "../ref-proxy.js" +import type { RefLeaf } from "../types.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type ComparisonOperand = + | RefProxy + | RefLeaf + | T + | BasicExpression + | undefined + | null + +type ComparisonOperandPrimitive = + | T + | BasicExpression + | undefined + | null + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function gte( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function gte( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function gte(left: Aggregate, right: any): BasicExpression +export function gte(left: any, right: any): BasicExpression { + return new Func(`gte`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function gteEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + + // In 3-valued logic, any comparison with null/undefined returns UNKNOWN + if (isUnknown(a) || isUnknown(b)) { + return null + } + + return a >= b + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`gte`, gteEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/ilike.ts b/packages/db/src/query/builder/operators/ilike.ts new file mode 100644 index 000000000..0bd293cde --- /dev/null +++ b/packages/db/src/query/builder/operators/ilike.ts @@ -0,0 +1,59 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import { evaluateLike } from "./like.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type StringRef = + | BasicExpression + | BasicExpression + | BasicExpression +type StringLike = StringRef | string | null | undefined | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function ilike( + left: StringLike, + right: StringLike +): BasicExpression { + return new Func(`ilike`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function ilikeEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const valueEvaluator = compiledArgs[0]! + const patternEvaluator = compiledArgs[1]! + + return (data: any) => { + const value = valueEvaluator(data) + const pattern = patternEvaluator(data) + // In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN + if (isUnknown(value) || isUnknown(pattern)) { + return null + } + return evaluateLike(value, pattern, true) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`ilike`, ilikeEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/in.ts b/packages/db/src/query/builder/operators/in.ts new file mode 100644 index 000000000..803a013f4 --- /dev/null +++ b/packages/db/src/query/builder/operators/in.ts @@ -0,0 +1,58 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function inArray( + value: ExpressionLike, + array: ExpressionLike +): BasicExpression { + return new Func(`in`, [toExpression(value), toExpression(array)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function inEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const valueEvaluator = compiledArgs[0]! + const arrayEvaluator = compiledArgs[1]! + + return (data: any) => { + const value = valueEvaluator(data) + const array = arrayEvaluator(data) + // In 3-valued logic, if the value is null/undefined, return UNKNOWN + if (isUnknown(value)) { + return null + } + if (!Array.isArray(array)) { + return false + } + return array.includes(value) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`in`, inEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/index.ts b/packages/db/src/query/builder/operators/index.ts new file mode 100644 index 000000000..240a61c03 --- /dev/null +++ b/packages/db/src/query/builder/operators/index.ts @@ -0,0 +1,38 @@ +// Re-export all operators +// Importing from here will auto-register all evaluators + +// Comparison operators +export { eq } from "./eq.js" +export { gt } from "./gt.js" +export { gte } from "./gte.js" +export { lt } from "./lt.js" +export { lte } from "./lte.js" + +// Boolean operators +export { and } from "./and.js" +export { or } from "./or.js" +export { not } from "./not.js" + +// Array operators +export { inArray } from "./in.js" + +// String pattern operators +export { like } from "./like.js" +export { ilike } from "./ilike.js" + +// String functions +export { upper } from "./upper.js" +export { lower } from "./lower.js" +export { length } from "./length.js" +export { concat } from "./concat.js" +export { coalesce } from "./coalesce.js" + +// Math functions +export { add } from "./add.js" +export { subtract } from "./subtract.js" +export { multiply } from "./multiply.js" +export { divide } from "./divide.js" + +// Null checking functions +export { isNull } from "./isNull.js" +export { isUndefined } from "./isUndefined.js" diff --git a/packages/db/src/query/builder/operators/isNull.ts b/packages/db/src/query/builder/operators/isNull.ts new file mode 100644 index 000000000..170ba9bf2 --- /dev/null +++ b/packages/db/src/query/builder/operators/isNull.ts @@ -0,0 +1,42 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function isNull(value: ExpressionLike): BasicExpression { + return new Func(`isNull`, [toExpression(value)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isNullEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const arg = compiledArgs[0]! + + return (data: any) => { + const value = arg(data) + return value === null + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`isNull`, isNullEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/isUndefined.ts b/packages/db/src/query/builder/operators/isUndefined.ts new file mode 100644 index 000000000..ab7c804ab --- /dev/null +++ b/packages/db/src/query/builder/operators/isUndefined.ts @@ -0,0 +1,42 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function isUndefined(value: ExpressionLike): BasicExpression { + return new Func(`isUndefined`, [toExpression(value)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUndefinedEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const arg = compiledArgs[0]! + + return (data: any) => { + const value = arg(data) + return value === undefined + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`isUndefined`, isUndefinedEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/length.ts b/packages/db/src/query/builder/operators/length.ts new file mode 100644 index 000000000..90147b327 --- /dev/null +++ b/packages/db/src/query/builder/operators/length.ts @@ -0,0 +1,53 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type to determine numeric function return type based on input nullability +type NumericFunctionReturnType<_T> = BasicExpression + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function length( + arg: T +): NumericFunctionReturnType { + return new Func(`length`, [toExpression(arg)]) as NumericFunctionReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function lengthEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const arg = compiledArgs[0]! + + return (data: any) => { + const value = arg(data) + if (typeof value === `string`) { + return value.length + } + if (Array.isArray(value)) { + return value.length + } + return 0 + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`length`, lengthEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/like.ts b/packages/db/src/query/builder/operators/like.ts new file mode 100644 index 000000000..80b365180 --- /dev/null +++ b/packages/db/src/query/builder/operators/like.ts @@ -0,0 +1,89 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type StringRef = + | BasicExpression + | BasicExpression + | BasicExpression +type StringLike = StringRef | string | null | undefined | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function like( + left: StringLike, + right: StringLike +): BasicExpression +export function like(left: any, right: any): BasicExpression { + return new Func(`like`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +/** + * Evaluates LIKE patterns + */ +function evaluateLike( + value: any, + pattern: any, + caseInsensitive: boolean +): boolean { + if (typeof value !== `string` || typeof pattern !== `string`) { + return false + } + + const searchValue = caseInsensitive ? value.toLowerCase() : value + const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern + + // Convert SQL LIKE pattern to regex + // First escape all regex special chars except % and _ + let regexPattern = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`) + + // Then convert SQL wildcards to regex + regexPattern = regexPattern.replace(/%/g, `.*`) // % matches any sequence + regexPattern = regexPattern.replace(/_/g, `.`) // _ matches any single char + + const regex = new RegExp(`^${regexPattern}$`) + return regex.test(searchValue) +} + +function likeEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const valueEvaluator = compiledArgs[0]! + const patternEvaluator = compiledArgs[1]! + + return (data: any) => { + const value = valueEvaluator(data) + const pattern = patternEvaluator(data) + // In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN + if (isUnknown(value) || isUnknown(pattern)) { + return null + } + return evaluateLike(value, pattern, false) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`like`, likeEvaluatorFactory) + +// Export for use by ilike +export { evaluateLike } diff --git a/packages/db/src/query/builder/operators/lower.ts b/packages/db/src/query/builder/operators/lower.ts new file mode 100644 index 000000000..fa1151431 --- /dev/null +++ b/packages/db/src/query/builder/operators/lower.ts @@ -0,0 +1,47 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type to determine string function return type based on input nullability +type StringFunctionReturnType<_T> = BasicExpression + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function lower( + arg: T +): StringFunctionReturnType { + return new Func(`lower`, [toExpression(arg)]) as StringFunctionReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function lowerEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const arg = compiledArgs[0]! + + return (data: any) => { + const value = arg(data) + return typeof value === `string` ? value.toLowerCase() : value + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`lower`, lowerEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/lt.ts b/packages/db/src/query/builder/operators/lt.ts new file mode 100644 index 000000000..f7401c213 --- /dev/null +++ b/packages/db/src/query/builder/operators/lt.ts @@ -0,0 +1,76 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { Aggregate, BasicExpression } from "../../ir.js" +import type { RefProxy } from "../ref-proxy.js" +import type { RefLeaf } from "../types.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type ComparisonOperand = + | RefProxy + | RefLeaf + | T + | BasicExpression + | undefined + | null + +type ComparisonOperandPrimitive = + | T + | BasicExpression + | undefined + | null + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function lt( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function lt( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function lt(left: Aggregate, right: any): BasicExpression +export function lt(left: any, right: any): BasicExpression { + return new Func(`lt`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function ltEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + + // In 3-valued logic, any comparison with null/undefined returns UNKNOWN + if (isUnknown(a) || isUnknown(b)) { + return null + } + + return a < b + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`lt`, ltEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/lte.ts b/packages/db/src/query/builder/operators/lte.ts new file mode 100644 index 000000000..eb40f6d7b --- /dev/null +++ b/packages/db/src/query/builder/operators/lte.ts @@ -0,0 +1,76 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { Aggregate, BasicExpression } from "../../ir.js" +import type { RefProxy } from "../ref-proxy.js" +import type { RefLeaf } from "../types.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +type ComparisonOperand = + | RefProxy + | RefLeaf + | T + | BasicExpression + | undefined + | null + +type ComparisonOperandPrimitive = + | T + | BasicExpression + | undefined + | null + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function lte( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function lte( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function lte(left: Aggregate, right: any): BasicExpression +export function lte(left: any, right: any): BasicExpression { + return new Func(`lte`, [toExpression(left), toExpression(right)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function lteEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + + // In 3-valued logic, any comparison with null/undefined returns UNKNOWN + if (isUnknown(a) || isUnknown(b)) { + return null + } + + return a <= b + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`lte`, lteEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/multiply.ts b/packages/db/src/query/builder/operators/multiply.ts new file mode 100644 index 000000000..d554689a4 --- /dev/null +++ b/packages/db/src/query/builder/operators/multiply.ts @@ -0,0 +1,55 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for binary numeric operations (combines nullability of both operands) +type BinaryNumericReturnType<_T1, _T2> = BasicExpression< + number | undefined | null +> + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function multiply( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func(`multiply`, [ + toExpression(left), + toExpression(right), + ]) as BinaryNumericReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function multiplyEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + return (a ?? 0) * (b ?? 0) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`multiply`, multiplyEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/not.ts b/packages/db/src/query/builder/operators/not.ts new file mode 100644 index 000000000..77d30edb3 --- /dev/null +++ b/packages/db/src/query/builder/operators/not.ts @@ -0,0 +1,53 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function not(value: ExpressionLike): BasicExpression { + return new Func(`not`, [toExpression(value)]) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function notEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const arg = compiledArgs[0]! + + return (data: any) => { + // 3-valued logic for NOT: + // - NOT null = null + // - NOT true = false + // - NOT false = true + const result = arg(data) + if (isUnknown(result)) { + return null + } + return !result + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`not`, notEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/or.ts b/packages/db/src/query/builder/operators/or.ts new file mode 100644 index 000000000..80a79b26b --- /dev/null +++ b/packages/db/src/query/builder/operators/or.ts @@ -0,0 +1,81 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +// Overloads for or() - support 2 or more arguments +export function or( + left: ExpressionLike, + right: ExpressionLike +): BasicExpression +export function or( + left: ExpressionLike, + right: ExpressionLike, + ...rest: Array +): BasicExpression +export function or( + left: ExpressionLike, + right: ExpressionLike, + ...rest: Array +): BasicExpression { + const allArgs = [left, right, ...rest] + return new Func( + `or`, + allArgs.map((arg) => toExpression(arg)) + ) +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function isUnknown(value: any): boolean { + return value === null || value === undefined +} + +function orEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + return (data: any) => { + // 3-valued logic for OR: + // - true OR anything = true (short-circuit) + // - null OR anything (except true) = null + // - false OR false = false + let hasUnknown = false + for (const compiledArg of compiledArgs) { + const result = compiledArg(data) + if (result === true) { + return true + } + if (isUnknown(result)) { + hasUnknown = true + } + } + // If we got here, no operand was true + // If any operand was null, return null (UNKNOWN) + if (hasUnknown) { + return null + } + + return false + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`or`, orEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/subtract.ts b/packages/db/src/query/builder/operators/subtract.ts new file mode 100644 index 000000000..d69bdfa33 --- /dev/null +++ b/packages/db/src/query/builder/operators/subtract.ts @@ -0,0 +1,55 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for binary numeric operations (combines nullability of both operands) +type BinaryNumericReturnType<_T1, _T2> = BasicExpression< + number | undefined | null +> + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function subtract( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func(`subtract`, [ + toExpression(left), + toExpression(right), + ]) as BinaryNumericReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function subtractEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const argA = compiledArgs[0]! + const argB = compiledArgs[1]! + + return (data: any) => { + const a = argA(data) + const b = argB(data) + return (a ?? 0) - (b ?? 0) + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`subtract`, subtractEvaluatorFactory) diff --git a/packages/db/src/query/builder/operators/upper.ts b/packages/db/src/query/builder/operators/upper.ts new file mode 100644 index 000000000..6bd99b7d0 --- /dev/null +++ b/packages/db/src/query/builder/operators/upper.ts @@ -0,0 +1,47 @@ +import { Func } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerOperator } from "../../compiler/registry.js" +import type { BasicExpression } from "../../ir.js" +import type { CompiledExpression } from "../../compiler/registry.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type to determine string function return type based on input nullability +type StringFunctionReturnType<_T> = BasicExpression + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function upper( + arg: T +): StringFunctionReturnType { + return new Func(`upper`, [toExpression(arg)]) as StringFunctionReturnType +} + +// ============================================================ +// EVALUATOR +// ============================================================ + +function upperEvaluatorFactory( + compiledArgs: Array, + _isSingleRow: boolean +): CompiledExpression { + const arg = compiledArgs[0]! + + return (data: any) => { + const value = arg(data) + return typeof value === `string` ? value.toUpperCase() : value + } +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerOperator(`upper`, upperEvaluatorFactory) diff --git a/packages/db/src/query/compiler/evaluators.ts b/packages/db/src/query/compiler/evaluators.ts index 19c69663d..015bdd9bd 100644 --- a/packages/db/src/query/compiler/evaluators.ts +++ b/packages/db/src/query/compiler/evaluators.ts @@ -3,16 +3,34 @@ import { UnknownExpressionTypeError, UnknownFunctionError, } from "../../errors.js" -import { areValuesEqual, normalizeValue } from "../../utils/comparison.js" +import { tryGetOperatorEvaluator } from "./registry.js" import type { BasicExpression, Func, PropRef } from "../ir.js" import type { NamespacedRow } from "../../types.js" -/** - * Helper function to check if a value is null or undefined (represents UNKNOWN in 3-valued logic) - */ -function isUnknown(value: any): boolean { - return value === null || value === undefined -} +// Import all operators to ensure they're registered before any compilation happens +// This ensures auto-registration works correctly +import "../builder/operators/eq.js" +import "../builder/operators/gt.js" +import "../builder/operators/gte.js" +import "../builder/operators/lt.js" +import "../builder/operators/lte.js" +import "../builder/operators/and.js" +import "../builder/operators/or.js" +import "../builder/operators/not.js" +import "../builder/operators/in.js" +import "../builder/operators/like.js" +import "../builder/operators/ilike.js" +import "../builder/operators/upper.js" +import "../builder/operators/lower.js" +import "../builder/operators/length.js" +import "../builder/operators/concat.js" +import "../builder/operators/coalesce.js" +import "../builder/operators/add.js" +import "../builder/operators/subtract.js" +import "../builder/operators/multiply.js" +import "../builder/operators/divide.js" +import "../builder/operators/isNull.js" +import "../builder/operators/isUndefined.js" /** * Converts a 3-valued logic result to a boolean for use in WHERE/HAVING filters. @@ -64,8 +82,9 @@ export function compileSingleRowExpression( /** * Internal unified expression compiler that handles both namespaced and single-row evaluation + * Exported for use by operator modules that need to compile their arguments. */ -function compileExpressionInternal( +export function compileExpressionInternal( expr: BasicExpression, isSingleRow: boolean ): (data: any) => any { @@ -160,326 +179,15 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { compileExpressionInternal(arg, isSingleRow) ) - switch (func.name) { - // Comparison operators - case `eq`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = normalizeValue(argA(data)) - const b = normalizeValue(argB(data)) - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - // Use areValuesEqual for proper Uint8Array/Buffer comparison - return areValuesEqual(a, b) - } - } - case `gt`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - return a > b - } - } - case `gte`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - return a >= b - } - } - case `lt`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - return a < b - } - } - case `lte`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - // In 3-valued logic, any comparison with null/undefined returns UNKNOWN - if (isUnknown(a) || isUnknown(b)) { - return null - } - return a <= b - } - } - - // Boolean operators - case `and`: - return (data) => { - // 3-valued logic for AND: - // - false AND anything = false (short-circuit) - // - null AND false = false - // - null AND anything (except false) = null - // - anything (except false) AND null = null - // - true AND true = true - let hasUnknown = false - for (const compiledArg of compiledArgs) { - const result = compiledArg(data) - if (result === false) { - return false - } - if (isUnknown(result)) { - hasUnknown = true - } - } - // If we got here, no operand was false - // If any operand was null, return null (UNKNOWN) - if (hasUnknown) { - return null - } - - return true - } - case `or`: - return (data) => { - // 3-valued logic for OR: - // - true OR anything = true (short-circuit) - // - null OR anything (except true) = null - // - false OR false = false - let hasUnknown = false - for (const compiledArg of compiledArgs) { - const result = compiledArg(data) - if (result === true) { - return true - } - if (isUnknown(result)) { - hasUnknown = true - } - } - // If we got here, no operand was true - // If any operand was null, return null (UNKNOWN) - if (hasUnknown) { - return null - } - - return false - } - case `not`: { - const arg = compiledArgs[0]! - return (data) => { - // 3-valued logic for NOT: - // - NOT null = null - // - NOT true = false - // - NOT false = true - const result = arg(data) - if (isUnknown(result)) { - return null - } - return !result - } - } - - // Array operators - case `in`: { - const valueEvaluator = compiledArgs[0]! - const arrayEvaluator = compiledArgs[1]! - return (data) => { - const value = valueEvaluator(data) - const array = arrayEvaluator(data) - // In 3-valued logic, if the value is null/undefined, return UNKNOWN - if (isUnknown(value)) { - return null - } - if (!Array.isArray(array)) { - return false - } - return array.includes(value) - } - } - - // String operators - case `like`: { - const valueEvaluator = compiledArgs[0]! - const patternEvaluator = compiledArgs[1]! - return (data) => { - const value = valueEvaluator(data) - const pattern = patternEvaluator(data) - // In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN - if (isUnknown(value) || isUnknown(pattern)) { - return null - } - return evaluateLike(value, pattern, false) - } - } - case `ilike`: { - const valueEvaluator = compiledArgs[0]! - const patternEvaluator = compiledArgs[1]! - return (data) => { - const value = valueEvaluator(data) - const pattern = patternEvaluator(data) - // In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN - if (isUnknown(value) || isUnknown(pattern)) { - return null - } - return evaluateLike(value, pattern, true) - } - } - - // String functions - case `upper`: { - const arg = compiledArgs[0]! - return (data) => { - const value = arg(data) - return typeof value === `string` ? value.toUpperCase() : value - } - } - case `lower`: { - const arg = compiledArgs[0]! - return (data) => { - const value = arg(data) - return typeof value === `string` ? value.toLowerCase() : value - } - } - case `length`: { - const arg = compiledArgs[0]! - return (data) => { - const value = arg(data) - if (typeof value === `string`) { - return value.length - } - if (Array.isArray(value)) { - return value.length - } - return 0 - } - } - case `concat`: - return (data) => { - return compiledArgs - .map((evaluator) => { - const arg = evaluator(data) - try { - return String(arg ?? ``) - } catch { - try { - return JSON.stringify(arg) || `` - } catch { - return `[object]` - } - } - }) - .join(``) - } - case `coalesce`: - return (data) => { - for (const evaluator of compiledArgs) { - const value = evaluator(data) - if (value !== null && value !== undefined) { - return value - } - } - return null - } - - // Math functions - case `add`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - return (a ?? 0) + (b ?? 0) - } - } - case `subtract`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - return (a ?? 0) - (b ?? 0) - } - } - case `multiply`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - return (a ?? 0) * (b ?? 0) - } - } - case `divide`: { - const argA = compiledArgs[0]! - const argB = compiledArgs[1]! - return (data) => { - const a = argA(data) - const b = argB(data) - const divisor = b ?? 0 - return divisor !== 0 ? (a ?? 0) / divisor : null - } - } - - // Null/undefined checking functions - case `isUndefined`: { - const arg = compiledArgs[0]! - return (data) => { - const value = arg(data) - return value === undefined - } - } - case `isNull`: { - const arg = compiledArgs[0]! - return (data) => { - const value = arg(data) - return value === null - } - } + // Try registry first (for migrated operators) + const evaluatorFactory = tryGetOperatorEvaluator(func.name) + if (evaluatorFactory) { + return evaluatorFactory(compiledArgs, isSingleRow) + } + // Fall back to switch for non-migrated operators (currently none, but kept for extensibility) + switch (func.name) { default: throw new UnknownFunctionError(func.name) } } - -/** - * Evaluates LIKE/ILIKE patterns - */ -function evaluateLike( - value: any, - pattern: any, - caseInsensitive: boolean -): boolean { - if (typeof value !== `string` || typeof pattern !== `string`) { - return false - } - - const searchValue = caseInsensitive ? value.toLowerCase() : value - const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern - - // Convert SQL LIKE pattern to regex - // First escape all regex special chars except % and _ - let regexPattern = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`) - - // Then convert SQL wildcards to regex - regexPattern = regexPattern.replace(/%/g, `.*`) // % matches any sequence - regexPattern = regexPattern.replace(/_/g, `.`) // _ matches any single char - - const regex = new RegExp(`^${regexPattern}$`) - return regex.test(searchValue) -} diff --git a/packages/db/src/query/compiler/registry.ts b/packages/db/src/query/compiler/registry.ts new file mode 100644 index 000000000..40bb21670 --- /dev/null +++ b/packages/db/src/query/compiler/registry.ts @@ -0,0 +1,52 @@ +import { UnknownFunctionError } from "../../errors.js" + +/** + * Type for a compiled expression evaluator + */ +export type CompiledExpression = (data: any) => any + +/** + * Factory function that creates an evaluator from compiled arguments + */ +export type EvaluatorFactory = ( + compiledArgs: Array, + isSingleRow: boolean +) => CompiledExpression + +/** + * Registry mapping operator names to their evaluator factories + */ +const operatorRegistry = new Map() + +/** + * Register an operator's evaluator factory. + * Called automatically when an operator module is imported. + */ +export function registerOperator( + name: string, + factory: EvaluatorFactory +): void { + operatorRegistry.set(name, factory) +} + +/** + * Get an operator's evaluator factory. + * Throws if the operator hasn't been registered. + */ +export function getOperatorEvaluator(name: string): EvaluatorFactory { + const factory = operatorRegistry.get(name) + if (!factory) { + throw new UnknownFunctionError(name) + } + return factory +} + +/** + * Try to get an operator's evaluator factory. + * Returns undefined if the operator hasn't been registered. + */ +export function tryGetOperatorEvaluator( + name: string +): EvaluatorFactory | undefined { + return operatorRegistry.get(name) +} diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index cb0e3d9e0..a86c058fd 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -12,36 +12,40 @@ export { type InferResultType, } from "./builder/index.js" -// Expression functions exports +// Expression functions exports - now from operator modules for tree-shaking export { - // Operators + // Comparison operators eq, gt, gte, lt, lte, + // Boolean operators and, or, not, + // Array/pattern operators inArray, like, ilike, + // Null checking isUndefined, isNull, - // Functions + // String functions upper, lower, length, concat, coalesce, + // Math functions add, - // Aggregates - count, - avg, - sum, - min, - max, -} from "./builder/functions.js" + subtract, + multiply, + divide, +} from "./builder/operators/index.js" + +// Aggregates remain in functions.ts (they're handled differently) +export { count, avg, sum, min, max } from "./builder/functions.js" // Ref proxy utilities export type { Ref } from "./builder/types.js" From c253c255a1230b7c9e2300c1955c280977792630 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Dec 2025 23:32:24 +0000 Subject: [PATCH 2/7] feat(db): export registerOperator for custom operators Export the registry API so users can create and register their own custom operators. Also adds a changeset for this patch. --- .changeset/auto-register-operators.md | 23 +++++++++++++++++++++++ packages/db/src/query/index.ts | 7 +++++++ 2 files changed, 30 insertions(+) create mode 100644 .changeset/auto-register-operators.md diff --git a/.changeset/auto-register-operators.md b/.changeset/auto-register-operators.md new file mode 100644 index 000000000..21ca6b57f --- /dev/null +++ b/.changeset/auto-register-operators.md @@ -0,0 +1,23 @@ +--- +"@tanstack/db": patch +--- + +Add auto-registering operators for tree-shaking support and custom operator extensibility. + +Each operator now bundles its builder function and evaluator in a single file, registering itself when imported. This enables: + +- **Tree-shaking**: Only operators you import are included in your bundle +- **Custom operators**: Use `registerOperator()` to add your own operators + +```typescript +import { registerOperator, type EvaluatorFactory } from '@tanstack/db' + +// Create a custom "between" operator +registerOperator('between', (compiledArgs, _isSingleRow) => { + const [valueEval, minEval, maxEval] = compiledArgs + return (data) => { + const value = valueEval!(data) + return value >= minEval!(data) && value <= maxEval!(data) + } +}) +``` diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index a86c058fd..418ac2e21 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -47,6 +47,13 @@ export { // Aggregates remain in functions.ts (they're handled differently) export { count, avg, sum, min, max } from "./builder/functions.js" +// Operator registry for custom operators +export { + registerOperator, + type EvaluatorFactory, + type CompiledExpression, +} from "./compiler/registry.js" + // Ref proxy utilities export type { Ref } from "./builder/types.js" From 78456e760abc5afbad7e27249857efa7b23af4cd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Dec 2025 23:35:10 +0000 Subject: [PATCH 3/7] feat(db): export registerOperator and add custom operators test - Export registerOperator, EvaluatorFactory, and CompiledExpression types - Add comprehensive tests for custom operator registration - Tests cover between, startsWith, isEmpty, modulo operators - Demonstrates full pattern: builder function + evaluator + registration --- .../query/compiler/custom-operators.test.ts | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 packages/db/tests/query/compiler/custom-operators.test.ts diff --git a/packages/db/tests/query/compiler/custom-operators.test.ts b/packages/db/tests/query/compiler/custom-operators.test.ts new file mode 100644 index 000000000..cebb0cbfe --- /dev/null +++ b/packages/db/tests/query/compiler/custom-operators.test.ts @@ -0,0 +1,250 @@ +import { describe, expect, it } from "vitest" +import { compileExpression } from "../../../src/query/compiler/evaluators.js" +import { registerOperator } from "../../../src/query/compiler/registry.js" +import { Func, PropRef, Value } from "../../../src/query/ir.js" +import { toExpression } from "../../../src/query/builder/ref-proxy.js" +import type { + CompiledExpression, + EvaluatorFactory, +} from "../../../src/query/compiler/registry.js" +import type { BasicExpression } from "../../../src/query/ir.js" + +describe(`custom operators`, () => { + describe(`registerOperator`, () => { + it(`allows registering a custom "between" operator`, () => { + // Register a custom "between" operator + const betweenFactory: EvaluatorFactory = ( + compiledArgs: Array, + _isSingleRow: boolean + ): CompiledExpression => { + const valueEval = compiledArgs[0]! + const minEval = compiledArgs[1]! + const maxEval = compiledArgs[2]! + + return (data: any) => { + const value = valueEval(data) + const min = minEval(data) + const max = maxEval(data) + + if (value === null || value === undefined) { + return null // 3-valued logic + } + + return value >= min && value <= max + } + } + + registerOperator(`between`, betweenFactory) + + // Test the custom operator + const func = new Func(`between`, [ + new Value(5), + new Value(1), + new Value(10), + ]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(true) + }) + + it(`custom "between" operator returns false when out of range`, () => { + const func = new Func(`between`, [ + new Value(15), + new Value(1), + new Value(10), + ]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(false) + }) + + it(`custom "between" operator handles null with 3-valued logic`, () => { + const func = new Func(`between`, [ + new Value(null), + new Value(1), + new Value(10), + ]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(null) + }) + + it(`allows registering a custom "startsWith" operator`, () => { + const startsWithFactory: EvaluatorFactory = ( + compiledArgs: Array, + _isSingleRow: boolean + ): CompiledExpression => { + const strEval = compiledArgs[0]! + const prefixEval = compiledArgs[1]! + + return (data: any) => { + const str = strEval(data) + const prefix = prefixEval(data) + + if (str === null || str === undefined) { + return null + } + if (typeof str !== `string` || typeof prefix !== `string`) { + return false + } + + return str.startsWith(prefix) + } + } + + registerOperator(`startsWith`, startsWithFactory) + + const func = new Func(`startsWith`, [ + new Value(`hello world`), + new Value(`hello`), + ]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(true) + }) + + it(`custom operator works with property references`, () => { + // Register a custom "isEmpty" operator + const isEmptyFactory: EvaluatorFactory = ( + compiledArgs: Array, + _isSingleRow: boolean + ): CompiledExpression => { + const valueEval = compiledArgs[0]! + + return (data: any) => { + const value = valueEval(data) + + if (value === null || value === undefined) { + return true + } + if (typeof value === `string`) { + return value.length === 0 + } + if (Array.isArray(value)) { + return value.length === 0 + } + + return false + } + } + + registerOperator(`isEmpty`, isEmptyFactory) + + // Test with a property reference + const func = new Func(`isEmpty`, [new PropRef([`users`, `name`])]) + const compiled = compileExpression(func) + + expect(compiled({ users: { name: `` } })).toBe(true) + expect(compiled({ users: { name: `John` } })).toBe(false) + expect(compiled({ users: { name: null } })).toBe(true) + }) + + it(`allows registering a custom "modulo" operator`, () => { + const moduloFactory: EvaluatorFactory = ( + compiledArgs: Array, + _isSingleRow: boolean + ): CompiledExpression => { + const leftEval = compiledArgs[0]! + const rightEval = compiledArgs[1]! + + return (data: any) => { + const left = leftEval(data) + const right = rightEval(data) + + if (left === null || left === undefined) { + return null + } + if (right === 0) { + return null // Division by zero + } + + return left % right + } + } + + registerOperator(`modulo`, moduloFactory) + + const func = new Func(`modulo`, [new Value(10), new Value(3)]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(1) + }) + + it(`custom operator can be used in nested expressions`, () => { + // Use the previously registered "between" with an "and" operator + const func = new Func(`and`, [ + new Func(`between`, [new Value(5), new Value(1), new Value(10)]), + new Func(`between`, [new Value(15), new Value(10), new Value(20)]), + ]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(true) + }) + + it(`custom operator can override built-in operator behavior`, () => { + // Register a custom version of "length" that handles objects + const customLengthFactory: EvaluatorFactory = ( + compiledArgs: Array, + _isSingleRow: boolean + ): CompiledExpression => { + const valueEval = compiledArgs[0]! + + return (data: any) => { + const value = valueEval(data) + + if (typeof value === `string`) { + return value.length + } + if (Array.isArray(value)) { + return value.length + } + if (value && typeof value === `object`) { + return Object.keys(value).length + } + + return 0 + } + } + + // This will override the built-in length operator + registerOperator(`customLength`, customLengthFactory) + + const func = new Func(`customLength`, [new Value({ a: 1, b: 2, c: 3 })]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(3) + }) + }) + + describe(`builder function pattern`, () => { + it(`can create a builder function for custom operators`, () => { + // This demonstrates the full pattern users would use + + // 1. Define the builder function (like eq, gt, etc.) + function between( + value: any, + min: any, + max: any + ): BasicExpression { + return new Func(`between`, [ + toExpression(value), + toExpression(min), + toExpression(max), + ]) + } + + // 2. The evaluator was already registered in previous tests + // In real usage, you'd register it alongside the builder + + // 3. Use it like any other operator + const expr = between(new PropRef([`users`, `age`]), 18, 65) + + // 4. Compile and execute + const compiled = compileExpression(expr) + + expect(compiled({ users: { age: 30 } })).toBe(true) + expect(compiled({ users: { age: 10 } })).toBe(false) + expect(compiled({ users: { age: 70 } })).toBe(false) + }) + }) +}) From 7c14cb5efa2847e6d9e1fc29e237641a7567d877 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Dec 2025 23:53:40 +0000 Subject: [PATCH 4/7] feat(db): add auto-registering aggregates for tree-shaking Extend the auto-registration pattern to aggregates (sum, count, avg, min, max): - Create aggregate-registry.ts with registerAggregate/getAggregateConfig - Split each aggregate into its own module with builder + auto-registration - Update group-by.ts to use registry lookup instead of switch statement - Export registerAggregate and types from public API - Add tests demonstrating custom aggregate registration This enables tree-shaking for aggregates and allows users to register custom aggregates like 'product', 'variance', etc. --- .changeset/auto-register-operators.md | 29 ++- .../db/src/query/builder/aggregates/avg.ts | 32 +++ .../db/src/query/builder/aggregates/count.ts | 29 +++ .../db/src/query/builder/aggregates/index.ts | 8 + .../db/src/query/builder/aggregates/max.ts | 32 +++ .../db/src/query/builder/aggregates/min.ts | 32 +++ .../db/src/query/builder/aggregates/sum.ts | 32 +++ .../src/query/compiler/aggregate-registry.ts | 57 +++++ packages/db/src/query/compiler/group-by.ts | 85 +++--- packages/db/src/query/index.ts | 12 +- .../query/compiler/custom-aggregates.test.ts | 242 ++++++++++++++++++ 11 files changed, 543 insertions(+), 47 deletions(-) create mode 100644 packages/db/src/query/builder/aggregates/avg.ts create mode 100644 packages/db/src/query/builder/aggregates/count.ts create mode 100644 packages/db/src/query/builder/aggregates/index.ts create mode 100644 packages/db/src/query/builder/aggregates/max.ts create mode 100644 packages/db/src/query/builder/aggregates/min.ts create mode 100644 packages/db/src/query/builder/aggregates/sum.ts create mode 100644 packages/db/src/query/compiler/aggregate-registry.ts create mode 100644 packages/db/tests/query/compiler/custom-aggregates.test.ts diff --git a/.changeset/auto-register-operators.md b/.changeset/auto-register-operators.md index 21ca6b57f..dc407b449 100644 --- a/.changeset/auto-register-operators.md +++ b/.changeset/auto-register-operators.md @@ -2,17 +2,18 @@ "@tanstack/db": patch --- -Add auto-registering operators for tree-shaking support and custom operator extensibility. +Add auto-registering operators and aggregates for tree-shaking support and custom extensibility. -Each operator now bundles its builder function and evaluator in a single file, registering itself when imported. This enables: +Each operator and aggregate now bundles its builder function and evaluator in a single file, registering itself when imported. This enables: -- **Tree-shaking**: Only operators you import are included in your bundle +- **Tree-shaking**: Only operators/aggregates you import are included in your bundle - **Custom operators**: Use `registerOperator()` to add your own operators +- **Custom aggregates**: Use `registerAggregate()` to add your own aggregate functions +**Custom Operator Example:** ```typescript import { registerOperator, type EvaluatorFactory } from '@tanstack/db' -// Create a custom "between" operator registerOperator('between', (compiledArgs, _isSingleRow) => { const [valueEval, minEval, maxEval] = compiledArgs return (data) => { @@ -21,3 +22,23 @@ registerOperator('between', (compiledArgs, _isSingleRow) => { } }) ``` + +**Custom Aggregate Example:** +```typescript +import { registerAggregate, type ValueExtractor } from '@tanstack/db' + +// Custom "product" aggregate that multiplies values +registerAggregate('product', { + factory: (valueExtractor: ValueExtractor) => ({ + preMap: valueExtractor, + reduce: (values) => { + let product = 1 + for (const [value, multiplicity] of values) { + for (let i = 0; i < multiplicity; i++) product *= value + } + return product + }, + }), + valueTransform: 'numeric', +}) +``` diff --git a/packages/db/src/query/builder/aggregates/avg.ts b/packages/db/src/query/builder/aggregates/avg.ts new file mode 100644 index 000000000..34bc77fc6 --- /dev/null +++ b/packages/db/src/query/builder/aggregates/avg.ts @@ -0,0 +1,32 @@ +import { groupByOperators } from "@tanstack/db-ivm" +import { Aggregate } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerAggregate } from "../../compiler/aggregate-registry.js" +import type { BasicExpression } from "../../ir.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for aggregate return type +type AggregateReturnType<_T> = Aggregate + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function avg(arg: T): AggregateReturnType { + return new Aggregate(`avg`, [toExpression(arg)]) as AggregateReturnType +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerAggregate(`avg`, { + factory: groupByOperators.avg, + valueTransform: `numeric`, +}) diff --git a/packages/db/src/query/builder/aggregates/count.ts b/packages/db/src/query/builder/aggregates/count.ts new file mode 100644 index 000000000..bc1f7dd49 --- /dev/null +++ b/packages/db/src/query/builder/aggregates/count.ts @@ -0,0 +1,29 @@ +import { groupByOperators } from "@tanstack/db-ivm" +import { Aggregate } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerAggregate } from "../../compiler/aggregate-registry.js" +import type { BasicExpression } from "../../ir.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function count(arg: ExpressionLike): Aggregate { + return new Aggregate(`count`, [toExpression(arg)]) +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerAggregate(`count`, { + factory: groupByOperators.count, + valueTransform: `raw`, +}) diff --git a/packages/db/src/query/builder/aggregates/index.ts b/packages/db/src/query/builder/aggregates/index.ts new file mode 100644 index 000000000..4fb5845f6 --- /dev/null +++ b/packages/db/src/query/builder/aggregates/index.ts @@ -0,0 +1,8 @@ +// Re-export all aggregates +// Importing from here will auto-register all aggregate evaluators + +export { sum } from "./sum.js" +export { count } from "./count.js" +export { avg } from "./avg.js" +export { min } from "./min.js" +export { max } from "./max.js" diff --git a/packages/db/src/query/builder/aggregates/max.ts b/packages/db/src/query/builder/aggregates/max.ts new file mode 100644 index 000000000..1cc12e481 --- /dev/null +++ b/packages/db/src/query/builder/aggregates/max.ts @@ -0,0 +1,32 @@ +import { groupByOperators } from "@tanstack/db-ivm" +import { Aggregate } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerAggregate } from "../../compiler/aggregate-registry.js" +import type { BasicExpression } from "../../ir.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for aggregate return type +type AggregateReturnType<_T> = Aggregate + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function max(arg: T): AggregateReturnType { + return new Aggregate(`max`, [toExpression(arg)]) as AggregateReturnType +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerAggregate(`max`, { + factory: groupByOperators.max, + valueTransform: `numericOrDate`, +}) diff --git a/packages/db/src/query/builder/aggregates/min.ts b/packages/db/src/query/builder/aggregates/min.ts new file mode 100644 index 000000000..8dd49d279 --- /dev/null +++ b/packages/db/src/query/builder/aggregates/min.ts @@ -0,0 +1,32 @@ +import { groupByOperators } from "@tanstack/db-ivm" +import { Aggregate } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerAggregate } from "../../compiler/aggregate-registry.js" +import type { BasicExpression } from "../../ir.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for aggregate return type +type AggregateReturnType<_T> = Aggregate + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function min(arg: T): AggregateReturnType { + return new Aggregate(`min`, [toExpression(arg)]) as AggregateReturnType +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerAggregate(`min`, { + factory: groupByOperators.min, + valueTransform: `numericOrDate`, +}) diff --git a/packages/db/src/query/builder/aggregates/sum.ts b/packages/db/src/query/builder/aggregates/sum.ts new file mode 100644 index 000000000..1bcd7025d --- /dev/null +++ b/packages/db/src/query/builder/aggregates/sum.ts @@ -0,0 +1,32 @@ +import { groupByOperators } from "@tanstack/db-ivm" +import { Aggregate } from "../../ir.js" +import { toExpression } from "../ref-proxy.js" +import { registerAggregate } from "../../compiler/aggregate-registry.js" +import type { BasicExpression } from "../../ir.js" + +// ============================================================ +// TYPES +// ============================================================ + +// Helper type for any expression-like value +type ExpressionLike = BasicExpression | any + +// Helper type for aggregate return type +type AggregateReturnType<_T> = Aggregate + +// ============================================================ +// BUILDER FUNCTION +// ============================================================ + +export function sum(arg: T): AggregateReturnType { + return new Aggregate(`sum`, [toExpression(arg)]) as AggregateReturnType +} + +// ============================================================ +// AUTO-REGISTRATION +// ============================================================ + +registerAggregate(`sum`, { + factory: groupByOperators.sum, + valueTransform: `numeric`, +}) diff --git a/packages/db/src/query/compiler/aggregate-registry.ts b/packages/db/src/query/compiler/aggregate-registry.ts new file mode 100644 index 000000000..227a4c471 --- /dev/null +++ b/packages/db/src/query/compiler/aggregate-registry.ts @@ -0,0 +1,57 @@ +import { UnsupportedAggregateFunctionError } from "../../errors.js" +import type { NamespacedRow } from "../../types.js" + +/** + * Value extractor type - extracts a value from a namespaced row + */ +export type ValueExtractor = (entry: [string, NamespacedRow]) => any + +/** + * Aggregate function factory - creates an IVM aggregate from a value extractor + */ +export type AggregateFactory = (valueExtractor: ValueExtractor) => any + +/** + * Configuration for how to create a value extractor for this aggregate + */ +export interface AggregateConfig { + /** The IVM aggregate function factory */ + factory: AggregateFactory + /** How to transform the compiled expression value */ + valueTransform: `numeric` | `numericOrDate` | `raw` +} + +/** + * Registry mapping aggregate names to their configurations + */ +const aggregateRegistry = new Map() + +/** + * Register an aggregate function. + * Called automatically when an aggregate module is imported. + */ +export function registerAggregate(name: string, config: AggregateConfig): void { + aggregateRegistry.set(name.toLowerCase(), config) +} + +/** + * Get an aggregate's configuration. + * Throws if the aggregate hasn't been registered. + */ +export function getAggregateConfig(name: string): AggregateConfig { + const config = aggregateRegistry.get(name.toLowerCase()) + if (!config) { + throw new UnsupportedAggregateFunctionError(name) + } + return config +} + +/** + * Try to get an aggregate's configuration. + * Returns undefined if the aggregate hasn't been registered. + */ +export function tryGetAggregateConfig( + name: string +): AggregateConfig | undefined { + return aggregateRegistry.get(name.toLowerCase()) +} diff --git a/packages/db/src/query/compiler/group-by.ts b/packages/db/src/query/compiler/group-by.ts index 5101245bb..549d983f1 100644 --- a/packages/db/src/query/compiler/group-by.ts +++ b/packages/db/src/query/compiler/group-by.ts @@ -1,12 +1,12 @@ -import { filter, groupBy, groupByOperators, map } from "@tanstack/db-ivm" +import { filter, groupBy, map } from "@tanstack/db-ivm" import { Func, PropRef, getHavingExpression } from "../ir.js" import { AggregateFunctionNotInSelectError, NonAggregateExpressionNotInGroupByError, UnknownHavingExpressionTypeError, - UnsupportedAggregateFunctionError, } from "../../errors.js" import { compileExpression, toBooleanPredicate } from "./evaluators.js" +import { getAggregateConfig } from "./aggregate-registry.js" import type { Aggregate, BasicExpression, @@ -16,7 +16,12 @@ import type { } from "../ir.js" import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js" -const { sum, count, avg, min, max } = groupByOperators +// Import all aggregates to ensure they're registered before any compilation happens +import "../builder/aggregates/sum.js" +import "../builder/aggregates/count.js" +import "../builder/aggregates/avg.js" +import "../builder/aggregates/min.js" +import "../builder/aggregates/max.js" /** * Interface for caching the mapping between GROUP BY expressions and SELECT expressions @@ -342,46 +347,44 @@ function getAggregateFunction(aggExpr: Aggregate) { // Pre-compile the value extractor expression const compiledExpr = compileExpression(aggExpr.args[0]!) - // Create a value extractor function for the expression to aggregate - const valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => { - const value = compiledExpr(namespacedRow) - // Ensure we return a number for numeric aggregate functions - return typeof value === `number` ? value : value != null ? Number(value) : 0 - } - - // Create a value extractor function for the expression to aggregate - const valueExtractorWithDate = ([, namespacedRow]: [ - string, - NamespacedRow, - ]) => { - const value = compiledExpr(namespacedRow) - return typeof value === `number` || value instanceof Date - ? value - : value != null - ? Number(value) - : 0 - } - - // Create a raw value extractor function for the expression to aggregate - const rawValueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => { - return compiledExpr(namespacedRow) - } - - // Return the appropriate aggregate function - switch (aggExpr.name.toLowerCase()) { - case `sum`: - return sum(valueExtractor) - case `count`: - return count(rawValueExtractor) - case `avg`: - return avg(valueExtractor) - case `min`: - return min(valueExtractorWithDate) - case `max`: - return max(valueExtractorWithDate) + // Get the aggregate configuration from registry + const config = getAggregateConfig(aggExpr.name) + + // Create the appropriate value extractor based on the config + let valueExtractor: (entry: [string, NamespacedRow]) => any + + switch (config.valueTransform) { + case `numeric`: + valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => { + const value = compiledExpr(namespacedRow) + // Ensure we return a number for numeric aggregate functions + return typeof value === `number` + ? value + : value != null + ? Number(value) + : 0 + } + break + case `numericOrDate`: + valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => { + const value = compiledExpr(namespacedRow) + return typeof value === `number` || value instanceof Date + ? value + : value != null + ? Number(value) + : 0 + } + break + case `raw`: default: - throw new UnsupportedAggregateFunctionError(aggExpr.name) + valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => { + return compiledExpr(namespacedRow) + } + break } + + // Return the aggregate function using the registered factory + return config.factory(valueExtractor) } /** diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 418ac2e21..13c38e552 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -44,8 +44,8 @@ export { divide, } from "./builder/operators/index.js" -// Aggregates remain in functions.ts (they're handled differently) -export { count, avg, sum, min, max } from "./builder/functions.js" +// Aggregates - now from aggregate modules for tree-shaking +export { count, avg, sum, min, max } from "./builder/aggregates/index.js" // Operator registry for custom operators export { @@ -54,6 +54,14 @@ export { type CompiledExpression, } from "./compiler/registry.js" +// Aggregate registry for custom aggregates +export { + registerAggregate, + type AggregateConfig, + type AggregateFactory, + type ValueExtractor, +} from "./compiler/aggregate-registry.js" + // Ref proxy utilities export type { Ref } from "./builder/types.js" diff --git a/packages/db/tests/query/compiler/custom-aggregates.test.ts b/packages/db/tests/query/compiler/custom-aggregates.test.ts new file mode 100644 index 000000000..a2ece2f32 --- /dev/null +++ b/packages/db/tests/query/compiler/custom-aggregates.test.ts @@ -0,0 +1,242 @@ +import { beforeAll, describe, expect, it } from "vitest" +import { createCollection } from "../../../src/collection/index.js" +import { + avg, + count, + createLiveQueryCollection, + sum, +} from "../../../src/query/index.js" +import { Aggregate } from "../../../src/query/ir.js" +import { toExpression } from "../../../src/query/builder/ref-proxy.js" +import { + getAggregateConfig, + registerAggregate, +} from "../../../src/query/compiler/aggregate-registry.js" +import { mockSyncCollectionOptions } from "../../utils.js" +import type { ValueExtractor } from "../../../src/query/compiler/aggregate-registry.js" + +interface TestItem { + id: number + category: string + price: number + quantity: number +} + +const sampleItems: Array = [ + { id: 1, category: `A`, price: 10, quantity: 2 }, + { id: 2, category: `A`, price: 20, quantity: 3 }, + { id: 3, category: `B`, price: 15, quantity: 1 }, + { id: 4, category: `B`, price: 25, quantity: 4 }, +] + +function createTestCollection() { + return createCollection( + mockSyncCollectionOptions({ + id: `test-custom-aggregates`, + getKey: (item) => item.id, + initialData: sampleItems, + }) + ) +} + +// Custom aggregate builder function (follows the same pattern as sum, count, etc.) +function product(arg: T): Aggregate { + return new Aggregate(`product`, [toExpression(arg)]) +} + +function variance(arg: T): Aggregate { + return new Aggregate(`variance`, [toExpression(arg)]) +} + +describe(`Custom Aggregates`, () => { + beforeAll(() => { + // Register custom aggregates for testing + // Aggregate functions must implement the IVM aggregate interface: + // { preMap: (data) => V, reduce: (values: [V, multiplicity][]) => V, postMap?: (V) => R } + + // Custom product aggregate: multiplies all values together + registerAggregate(`product`, { + factory: (valueExtractor: ValueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => { + let product = 1 + for (const [value, multiplicity] of values) { + // For positive multiplicity, multiply the value that many times + // For negative multiplicity, divide (inverse operation for IVM) + if (multiplicity > 0) { + for (let i = 0; i < multiplicity; i++) { + product *= value + } + } else if (multiplicity < 0) { + for (let i = 0; i < -multiplicity; i++) { + product /= value + } + } + } + return product + }, + }), + valueTransform: `numeric`, + }) + + // Custom variance aggregate (simplified - population variance) + // Stores { sum, sumSq, n } to compute variance + registerAggregate(`variance`, { + factory: (valueExtractor: ValueExtractor) => ({ + preMap: (data: any) => { + const value = valueExtractor(data) + return { sum: value, sumSq: value * value, n: 1 } + }, + reduce: ( + values: Array<[{ sum: number; sumSq: number; n: number }, number]> + ) => { + let totalSum = 0 + let totalSumSq = 0 + let totalN = 0 + for (const [{ sum, sumSq, n }, multiplicity] of values) { + totalSum += sum * multiplicity + totalSumSq += sumSq * multiplicity + totalN += n * multiplicity + } + return { sum: totalSum, sumSq: totalSumSq, n: totalN } + }, + postMap: (acc: { sum: number; sumSq: number; n: number }) => { + if (acc.n === 0) return 0 + const mean = acc.sum / acc.n + return acc.sumSq / acc.n - mean * mean + }, + }), + valueTransform: `raw`, // We handle the transformation in preMap + }) + }) + + describe(`registerAggregate`, () => { + it(`registers a custom aggregate in the registry`, () => { + const config = getAggregateConfig(`product`) + expect(config).toBeDefined() + expect(config.valueTransform).toBe(`numeric`) + expect(typeof config.factory).toBe(`function`) + }) + + it(`retrieves custom aggregate config (case-insensitive)`, () => { + const config1 = getAggregateConfig(`Product`) + const config2 = getAggregateConfig(`PRODUCT`) + expect(config1).toBeDefined() + expect(config2).toBeDefined() + }) + }) + + describe(`custom aggregate builder functions`, () => { + it(`creates an Aggregate IR node for product`, () => { + const agg = product(10) + expect(agg.type).toBe(`agg`) + expect(agg.name).toBe(`product`) + expect(agg.args).toHaveLength(1) + }) + + it(`creates an Aggregate IR node for variance`, () => { + const agg = variance(10) + expect(agg.type).toBe(`agg`) + expect(agg.name).toBe(`variance`) + expect(agg.args).toHaveLength(1) + }) + }) + + describe(`custom aggregates in queries`, () => { + it(`product aggregate multiplies values in a group`, () => { + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + priceProduct: product(items.price), + })), + }) + + expect(result.size).toBe(2) + + const categoryA = result.get(`A`) + const categoryB = result.get(`B`) + + expect(categoryA?.priceProduct).toBe(200) // 10 * 20 + expect(categoryB?.priceProduct).toBe(375) // 15 * 25 + }) + + it(`variance aggregate calculates population variance`, () => { + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + priceVariance: variance(items.price), + })), + }) + + expect(result.size).toBe(2) + + // Category A: prices 10, 20 -> mean = 15, variance = ((10-15)² + (20-15)²) / 2 = 25 + const categoryA = result.get(`A`) + expect(categoryA?.priceVariance).toBe(25) + + // Category B: prices 15, 25 -> mean = 20, variance = ((15-20)² + (25-20)²) / 2 = 25 + const categoryB = result.get(`B`) + expect(categoryB?.priceVariance).toBe(25) + }) + + it(`custom aggregates work alongside built-in aggregates`, () => { + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(({ items }) => items.category) + .select(({ items }) => ({ + category: items.category, + totalPrice: sum(items.price), + avgPrice: avg(items.price), + itemCount: count(items.id), + priceProduct: product(items.price), + })), + }) + + expect(result.size).toBe(2) + + const categoryA = result.get(`A`) + expect(categoryA?.totalPrice).toBe(30) // 10 + 20 + expect(categoryA?.avgPrice).toBe(15) // (10 + 20) / 2 + expect(categoryA?.itemCount).toBe(2) + expect(categoryA?.priceProduct).toBe(200) // 10 * 20 + }) + + it(`custom aggregates work with single-group aggregation (empty GROUP BY)`, () => { + const collection = createTestCollection() + + const result = createLiveQueryCollection({ + startSync: true, + query: (q) => + q + .from({ items: collection }) + .groupBy(() => ({})) + .select(({ items }) => ({ + totalProduct: product(items.price), + })), + }) + + expect(result.size).toBe(1) + // 10 * 20 * 15 * 25 = 75000 + expect(result.toArray[0]?.totalProduct).toBe(75000) + }) + }) +}) From 67eb3241ed3bb802160e8ce1fa1a8f4059b6460e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Dec 2025 00:02:07 +0000 Subject: [PATCH 5/7] style: format changeset --- .changeset/auto-register-operators.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.changeset/auto-register-operators.md b/.changeset/auto-register-operators.md index dc407b449..a18cae698 100644 --- a/.changeset/auto-register-operators.md +++ b/.changeset/auto-register-operators.md @@ -11,10 +11,11 @@ Each operator and aggregate now bundles its builder function and evaluator in a - **Custom aggregates**: Use `registerAggregate()` to add your own aggregate functions **Custom Operator Example:** + ```typescript -import { registerOperator, type EvaluatorFactory } from '@tanstack/db' +import { registerOperator, type EvaluatorFactory } from "@tanstack/db" -registerOperator('between', (compiledArgs, _isSingleRow) => { +registerOperator("between", (compiledArgs, _isSingleRow) => { const [valueEval, minEval, maxEval] = compiledArgs return (data) => { const value = valueEval!(data) @@ -24,11 +25,12 @@ registerOperator('between', (compiledArgs, _isSingleRow) => { ``` **Custom Aggregate Example:** + ```typescript -import { registerAggregate, type ValueExtractor } from '@tanstack/db' +import { registerAggregate, type ValueExtractor } from "@tanstack/db" // Custom "product" aggregate that multiplies values -registerAggregate('product', { +registerAggregate("product", { factory: (valueExtractor: ValueExtractor) => ({ preMap: valueExtractor, reduce: (values) => { @@ -39,6 +41,6 @@ registerAggregate('product', { return product }, }), - valueTransform: 'numeric', + valueTransform: "numeric", }) ``` From ce195cb1ef48b44ebf2d97913618e060f6efe9af Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Dec 2025 00:36:52 +0000 Subject: [PATCH 6/7] fix(db): enable true lazy registration for operators/aggregates - Remove eager imports from evaluators.ts and group-by.ts - Make functions.ts re-export from operator/aggregate modules - Add shared types.ts for type helpers preserving nullability - Update test files to import operators for direct IR testing Now operators/aggregates are only loaded when user imports them, enabling true tree-shaking. The compiler no longer pre-loads all evaluators - they register when their builder functions are imported. --- .../db/src/query/builder/aggregates/avg.ts | 12 +- .../db/src/query/builder/aggregates/count.ts | 9 +- .../db/src/query/builder/aggregates/max.ts | 12 +- .../db/src/query/builder/aggregates/min.ts | 12 +- .../db/src/query/builder/aggregates/sum.ts | 12 +- packages/db/src/query/builder/functions.ts | 360 ++---------------- .../db/src/query/builder/operators/add.ts | 14 +- .../db/src/query/builder/operators/length.ts | 12 +- .../db/src/query/builder/operators/lower.ts | 12 +- .../db/src/query/builder/operators/types.ts | 120 ++++++ .../db/src/query/builder/operators/upper.ts | 12 +- packages/db/src/query/compiler/evaluators.ts | 28 +- packages/db/src/query/compiler/group-by.ts | 10 +- .../query/compiler/custom-operators.test.ts | 3 + .../tests/query/compiler/evaluators.test.ts | 3 + .../db/tests/query/compiler/select.test.ts | 3 + 16 files changed, 181 insertions(+), 453 deletions(-) create mode 100644 packages/db/src/query/builder/operators/types.ts diff --git a/packages/db/src/query/builder/aggregates/avg.ts b/packages/db/src/query/builder/aggregates/avg.ts index 34bc77fc6..e89931b69 100644 --- a/packages/db/src/query/builder/aggregates/avg.ts +++ b/packages/db/src/query/builder/aggregates/avg.ts @@ -2,17 +2,7 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerAggregate } from "../../compiler/aggregate-registry.js" -import type { BasicExpression } from "../../ir.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for aggregate return type -type AggregateReturnType<_T> = Aggregate +import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/aggregates/count.ts b/packages/db/src/query/builder/aggregates/count.ts index bc1f7dd49..561dd7b35 100644 --- a/packages/db/src/query/builder/aggregates/count.ts +++ b/packages/db/src/query/builder/aggregates/count.ts @@ -2,14 +2,7 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerAggregate } from "../../compiler/aggregate-registry.js" -import type { BasicExpression } from "../../ir.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any +import type { ExpressionLike } from "../operators/types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/aggregates/max.ts b/packages/db/src/query/builder/aggregates/max.ts index 1cc12e481..e9b396c2e 100644 --- a/packages/db/src/query/builder/aggregates/max.ts +++ b/packages/db/src/query/builder/aggregates/max.ts @@ -2,17 +2,7 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerAggregate } from "../../compiler/aggregate-registry.js" -import type { BasicExpression } from "../../ir.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for aggregate return type -type AggregateReturnType<_T> = Aggregate +import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/aggregates/min.ts b/packages/db/src/query/builder/aggregates/min.ts index 8dd49d279..f1f3852cd 100644 --- a/packages/db/src/query/builder/aggregates/min.ts +++ b/packages/db/src/query/builder/aggregates/min.ts @@ -2,17 +2,7 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerAggregate } from "../../compiler/aggregate-registry.js" -import type { BasicExpression } from "../../ir.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for aggregate return type -type AggregateReturnType<_T> = Aggregate +import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/aggregates/sum.ts b/packages/db/src/query/builder/aggregates/sum.ts index 1bcd7025d..2aa731a1c 100644 --- a/packages/db/src/query/builder/aggregates/sum.ts +++ b/packages/db/src/query/builder/aggregates/sum.ts @@ -2,17 +2,7 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerAggregate } from "../../compiler/aggregate-registry.js" -import type { BasicExpression } from "../../ir.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for aggregate return type -type AggregateReturnType<_T> = Aggregate +import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/functions.ts b/packages/db/src/query/builder/functions.ts index eca3172c0..619a66381 100644 --- a/packages/db/src/query/builder/functions.ts +++ b/packages/db/src/query/builder/functions.ts @@ -1,328 +1,35 @@ -import { Aggregate, Func } from "../ir" -import { toExpression } from "./ref-proxy.js" -import type { BasicExpression } from "../ir" -import type { RefProxy } from "./ref-proxy.js" -import type { RefLeaf } from "./types.js" - -type StringRef = - | RefLeaf - | RefLeaf - | RefLeaf -type StringRefProxy = - | RefProxy - | RefProxy - | RefProxy -type StringBasicExpression = - | BasicExpression - | BasicExpression - | BasicExpression -type StringLike = - | StringRef - | StringRefProxy - | StringBasicExpression - | string - | null - | undefined - -type ComparisonOperand = - | RefProxy - | RefLeaf - | T - | BasicExpression - | undefined - | null -type ComparisonOperandPrimitive = - | T - | BasicExpression - | undefined - | null - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | RefProxy | RefLeaf | any - -// Helper type to extract the underlying type from various expression types -type ExtractType = - T extends RefProxy - ? U - : T extends RefLeaf - ? U - : T extends BasicExpression - ? U - : T - -// Helper type to determine aggregate return type based on input nullability -type AggregateReturnType = - ExtractType extends infer U - ? U extends number | undefined | null | Date | bigint - ? Aggregate - : Aggregate - : Aggregate - -// Helper type to determine string function return type based on input nullability -type StringFunctionReturnType = - ExtractType extends infer U - ? U extends string | undefined | null - ? BasicExpression - : BasicExpression - : BasicExpression - -// Helper type to determine numeric function return type based on input nullability -// This handles string, array, and number inputs for functions like length() -type NumericFunctionReturnType = - ExtractType extends infer U - ? U extends string | Array | undefined | null | number - ? BasicExpression> - : BasicExpression - : BasicExpression - -// Transform string/array types to number while preserving nullability -type MapToNumber = T extends string | Array - ? number - : T extends undefined - ? undefined - : T extends null - ? null - : T - -// Helper type for binary numeric operations (combines nullability of both operands) -type BinaryNumericReturnType = - ExtractType extends infer U1 - ? ExtractType extends infer U2 - ? U1 extends number - ? U2 extends number - ? BasicExpression - : U2 extends number | undefined - ? BasicExpression - : U2 extends number | null - ? BasicExpression - : BasicExpression - : U1 extends number | undefined - ? U2 extends number - ? BasicExpression - : U2 extends number | undefined - ? BasicExpression - : BasicExpression - : U1 extends number | null - ? U2 extends number - ? BasicExpression - : BasicExpression - : BasicExpression - : BasicExpression - : BasicExpression - -// Operators - -export function eq( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function eq( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function eq(left: Aggregate, right: any): BasicExpression -export function eq(left: any, right: any): BasicExpression { - return new Func(`eq`, [toExpression(left), toExpression(right)]) -} - -export function gt( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function gt( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function gt(left: Aggregate, right: any): BasicExpression -export function gt(left: any, right: any): BasicExpression { - return new Func(`gt`, [toExpression(left), toExpression(right)]) -} - -export function gte( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function gte( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function gte(left: Aggregate, right: any): BasicExpression -export function gte(left: any, right: any): BasicExpression { - return new Func(`gte`, [toExpression(left), toExpression(right)]) -} - -export function lt( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function lt( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function lt(left: Aggregate, right: any): BasicExpression -export function lt(left: any, right: any): BasicExpression { - return new Func(`lt`, [toExpression(left), toExpression(right)]) -} - -export function lte( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function lte( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function lte(left: Aggregate, right: any): BasicExpression -export function lte(left: any, right: any): BasicExpression { - return new Func(`lte`, [toExpression(left), toExpression(right)]) -} - -// Overloads for and() - support 2 or more arguments -export function and( - left: ExpressionLike, - right: ExpressionLike -): BasicExpression -export function and( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression -export function and( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression { - const allArgs = [left, right, ...rest] - return new Func( - `and`, - allArgs.map((arg) => toExpression(arg)) - ) -} - -// Overloads for or() - support 2 or more arguments -export function or( - left: ExpressionLike, - right: ExpressionLike -): BasicExpression -export function or( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression -export function or( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression { - const allArgs = [left, right, ...rest] - return new Func( - `or`, - allArgs.map((arg) => toExpression(arg)) - ) -} - -export function not(value: ExpressionLike): BasicExpression { - return new Func(`not`, [toExpression(value)]) -} - -// Null/undefined checking functions -export function isUndefined(value: ExpressionLike): BasicExpression { - return new Func(`isUndefined`, [toExpression(value)]) -} - -export function isNull(value: ExpressionLike): BasicExpression { - return new Func(`isNull`, [toExpression(value)]) -} - -export function inArray( - value: ExpressionLike, - array: ExpressionLike -): BasicExpression { - return new Func(`in`, [toExpression(value), toExpression(array)]) -} - -export function like( - left: StringLike, - right: StringLike -): BasicExpression -export function like(left: any, right: any): BasicExpression { - return new Func(`like`, [toExpression(left), toExpression(right)]) -} - -export function ilike( - left: StringLike, - right: StringLike -): BasicExpression { - return new Func(`ilike`, [toExpression(left), toExpression(right)]) -} - -// Functions - -export function upper( - arg: T -): StringFunctionReturnType { - return new Func(`upper`, [toExpression(arg)]) as StringFunctionReturnType -} - -export function lower( - arg: T -): StringFunctionReturnType { - return new Func(`lower`, [toExpression(arg)]) as StringFunctionReturnType -} - -export function length( - arg: T -): NumericFunctionReturnType { - return new Func(`length`, [toExpression(arg)]) as NumericFunctionReturnType -} - -export function concat( - ...args: Array -): BasicExpression { - return new Func( - `concat`, - args.map((arg) => toExpression(arg)) - ) -} - -export function coalesce(...args: Array): BasicExpression { - return new Func( - `coalesce`, - args.map((arg) => toExpression(arg)) - ) -} - -export function add( - left: T1, - right: T2 -): BinaryNumericReturnType { - return new Func(`add`, [ - toExpression(left), - toExpression(right), - ]) as BinaryNumericReturnType -} - -// Aggregates - -export function count(arg: ExpressionLike): Aggregate { - return new Aggregate(`count`, [toExpression(arg)]) -} - -export function avg(arg: T): AggregateReturnType { - return new Aggregate(`avg`, [toExpression(arg)]) as AggregateReturnType -} - -export function sum(arg: T): AggregateReturnType { - return new Aggregate(`sum`, [toExpression(arg)]) as AggregateReturnType -} - -export function min(arg: T): AggregateReturnType { - return new Aggregate(`min`, [toExpression(arg)]) as AggregateReturnType -} - -export function max(arg: T): AggregateReturnType { - return new Aggregate(`max`, [toExpression(arg)]) as AggregateReturnType -} +// Re-export all operators from their individual modules +// Each module auto-registers its evaluator when imported +export { eq } from "./operators/eq.js" +export { gt } from "./operators/gt.js" +export { gte } from "./operators/gte.js" +export { lt } from "./operators/lt.js" +export { lte } from "./operators/lte.js" +export { and } from "./operators/and.js" +export { or } from "./operators/or.js" +export { not } from "./operators/not.js" +export { inArray } from "./operators/in.js" +export { like } from "./operators/like.js" +export { ilike } from "./operators/ilike.js" +export { upper } from "./operators/upper.js" +export { lower } from "./operators/lower.js" +export { length } from "./operators/length.js" +export { concat } from "./operators/concat.js" +export { coalesce } from "./operators/coalesce.js" +export { add } from "./operators/add.js" +export { subtract } from "./operators/subtract.js" +export { multiply } from "./operators/multiply.js" +export { divide } from "./operators/divide.js" +export { isNull } from "./operators/isNull.js" +export { isUndefined } from "./operators/isUndefined.js" + +// Re-export all aggregates from their individual modules +// Each module auto-registers its config when imported +export { count } from "./aggregates/count.js" +export { avg } from "./aggregates/avg.js" +export { sum } from "./aggregates/sum.js" +export { min } from "./aggregates/min.js" +export { max } from "./aggregates/max.js" /** * List of comparison function names that can be used with indexes @@ -365,6 +72,9 @@ export const operators = [ `concat`, // Numeric functions `add`, + `subtract`, + `multiply`, + `divide`, // Utility functions `coalesce`, // Aggregate functions diff --git a/packages/db/src/query/builder/operators/add.ts b/packages/db/src/query/builder/operators/add.ts index 97113d48a..5e639c68b 100644 --- a/packages/db/src/query/builder/operators/add.ts +++ b/packages/db/src/query/builder/operators/add.ts @@ -1,20 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" import type { CompiledExpression } from "../../compiler/registry.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type for binary numeric operations (combines nullability of both operands) -type BinaryNumericReturnType<_T1, _T2> = BasicExpression< - number | undefined | null -> +import type { BinaryNumericReturnType, ExpressionLike } from "./types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/operators/length.ts b/packages/db/src/query/builder/operators/length.ts index 90147b327..9f34ddd1d 100644 --- a/packages/db/src/query/builder/operators/length.ts +++ b/packages/db/src/query/builder/operators/length.ts @@ -1,18 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" import type { CompiledExpression } from "../../compiler/registry.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type to determine numeric function return type based on input nullability -type NumericFunctionReturnType<_T> = BasicExpression +import type { ExpressionLike, NumericFunctionReturnType } from "./types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/operators/lower.ts b/packages/db/src/query/builder/operators/lower.ts index fa1151431..f67a787c4 100644 --- a/packages/db/src/query/builder/operators/lower.ts +++ b/packages/db/src/query/builder/operators/lower.ts @@ -1,18 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" import type { CompiledExpression } from "../../compiler/registry.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type to determine string function return type based on input nullability -type StringFunctionReturnType<_T> = BasicExpression +import type { ExpressionLike, StringFunctionReturnType } from "./types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/builder/operators/types.ts b/packages/db/src/query/builder/operators/types.ts new file mode 100644 index 000000000..9d5a509b0 --- /dev/null +++ b/packages/db/src/query/builder/operators/types.ts @@ -0,0 +1,120 @@ +/** + * Shared types for operator modules + * These helper types preserve nullability information in return types + */ + +import type { Aggregate, BasicExpression } from "../../ir.js" +import type { RefProxy } from "../ref-proxy.js" +import type { RefLeaf } from "../types.js" + +// String-like types +type StringRef = + | RefLeaf + | RefLeaf + | RefLeaf +type StringRefProxy = + | RefProxy + | RefProxy + | RefProxy +type StringBasicExpression = + | BasicExpression + | BasicExpression + | BasicExpression +export type StringLike = + | StringRef + | StringRefProxy + | StringBasicExpression + | string + | null + | undefined + +// Comparison operand types +export type ComparisonOperand = + | RefProxy + | RefLeaf + | T + | BasicExpression + | undefined + | null +export type ComparisonOperandPrimitive = + | T + | BasicExpression + | undefined + | null + +// Helper type for any expression-like value +export type ExpressionLike = + | BasicExpression + | RefProxy + | RefLeaf + | any + +// Helper type to extract the underlying type from various expression types +export type ExtractType = + T extends RefProxy + ? U + : T extends RefLeaf + ? U + : T extends BasicExpression + ? U + : T + +// Helper type to determine aggregate return type based on input nullability +export type AggregateReturnType = + ExtractType extends infer U + ? U extends number | undefined | null | Date | bigint + ? Aggregate + : Aggregate + : Aggregate + +// Helper type to determine string function return type based on input nullability +export type StringFunctionReturnType = + ExtractType extends infer U + ? U extends string | undefined | null + ? BasicExpression + : BasicExpression + : BasicExpression + +// Helper type to determine numeric function return type based on input nullability +// This handles string, array, and number inputs for functions like length() +export type NumericFunctionReturnType = + ExtractType extends infer U + ? U extends string | Array | undefined | null | number + ? BasicExpression> + : BasicExpression + : BasicExpression + +// Transform string/array types to number while preserving nullability +type MapToNumber = T extends string | Array + ? number + : T extends undefined + ? undefined + : T extends null + ? null + : T + +// Helper type for binary numeric operations (combines nullability of both operands) +export type BinaryNumericReturnType = + ExtractType extends infer U1 + ? ExtractType extends infer U2 + ? U1 extends number + ? U2 extends number + ? BasicExpression + : U2 extends number | undefined + ? BasicExpression + : U2 extends number | null + ? BasicExpression + : BasicExpression + : U1 extends number | undefined + ? U2 extends number + ? BasicExpression + : U2 extends number | undefined + ? BasicExpression + : BasicExpression + : U1 extends number | null + ? U2 extends number + ? BasicExpression + : BasicExpression + : BasicExpression + : BasicExpression + : BasicExpression diff --git a/packages/db/src/query/builder/operators/upper.ts b/packages/db/src/query/builder/operators/upper.ts index 6bd99b7d0..7090be19f 100644 --- a/packages/db/src/query/builder/operators/upper.ts +++ b/packages/db/src/query/builder/operators/upper.ts @@ -1,18 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" import type { CompiledExpression } from "../../compiler/registry.js" - -// ============================================================ -// TYPES -// ============================================================ - -// Helper type for any expression-like value -type ExpressionLike = BasicExpression | any - -// Helper type to determine string function return type based on input nullability -type StringFunctionReturnType<_T> = BasicExpression +import type { ExpressionLike, StringFunctionReturnType } from "./types.js" // ============================================================ // BUILDER FUNCTION diff --git a/packages/db/src/query/compiler/evaluators.ts b/packages/db/src/query/compiler/evaluators.ts index 015bdd9bd..97e138d38 100644 --- a/packages/db/src/query/compiler/evaluators.ts +++ b/packages/db/src/query/compiler/evaluators.ts @@ -7,30 +7,10 @@ import { tryGetOperatorEvaluator } from "./registry.js" import type { BasicExpression, Func, PropRef } from "../ir.js" import type { NamespacedRow } from "../../types.js" -// Import all operators to ensure they're registered before any compilation happens -// This ensures auto-registration works correctly -import "../builder/operators/eq.js" -import "../builder/operators/gt.js" -import "../builder/operators/gte.js" -import "../builder/operators/lt.js" -import "../builder/operators/lte.js" -import "../builder/operators/and.js" -import "../builder/operators/or.js" -import "../builder/operators/not.js" -import "../builder/operators/in.js" -import "../builder/operators/like.js" -import "../builder/operators/ilike.js" -import "../builder/operators/upper.js" -import "../builder/operators/lower.js" -import "../builder/operators/length.js" -import "../builder/operators/concat.js" -import "../builder/operators/coalesce.js" -import "../builder/operators/add.js" -import "../builder/operators/subtract.js" -import "../builder/operators/multiply.js" -import "../builder/operators/divide.js" -import "../builder/operators/isNull.js" -import "../builder/operators/isUndefined.js" +// Operators are lazily registered when imported by user code. +// Each operator file (e.g., eq.ts) auto-registers its evaluator on import. +// If a user uses an operator without importing it, compilation will fail +// with an UnknownFunctionError guiding them to import it. /** * Converts a 3-valued logic result to a boolean for use in WHERE/HAVING filters. diff --git a/packages/db/src/query/compiler/group-by.ts b/packages/db/src/query/compiler/group-by.ts index 549d983f1..192bd43ad 100644 --- a/packages/db/src/query/compiler/group-by.ts +++ b/packages/db/src/query/compiler/group-by.ts @@ -16,12 +16,10 @@ import type { } from "../ir.js" import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js" -// Import all aggregates to ensure they're registered before any compilation happens -import "../builder/aggregates/sum.js" -import "../builder/aggregates/count.js" -import "../builder/aggregates/avg.js" -import "../builder/aggregates/min.js" -import "../builder/aggregates/max.js" +// Aggregates are lazily registered when imported by user code. +// Each aggregate file (e.g., sum.ts) auto-registers its config on import. +// If a user uses an aggregate without importing it, compilation will fail +// with an UnsupportedAggregateFunctionError guiding them to import it. /** * Interface for caching the mapping between GROUP BY expressions and SELECT expressions diff --git a/packages/db/tests/query/compiler/custom-operators.test.ts b/packages/db/tests/query/compiler/custom-operators.test.ts index cebb0cbfe..fe3988d84 100644 --- a/packages/db/tests/query/compiler/custom-operators.test.ts +++ b/packages/db/tests/query/compiler/custom-operators.test.ts @@ -9,6 +9,9 @@ import type { } from "../../../src/query/compiler/registry.js" import type { BasicExpression } from "../../../src/query/ir.js" +// Import operators to register evaluators (needed for direct IR testing) +import "../../../src/query/builder/operators/index.js" + describe(`custom operators`, () => { describe(`registerOperator`, () => { it(`allows registering a custom "between" operator`, () => { diff --git a/packages/db/tests/query/compiler/evaluators.test.ts b/packages/db/tests/query/compiler/evaluators.test.ts index 64a867196..8551a8a2a 100644 --- a/packages/db/tests/query/compiler/evaluators.test.ts +++ b/packages/db/tests/query/compiler/evaluators.test.ts @@ -3,6 +3,9 @@ import { compileExpression } from "../../../src/query/compiler/evaluators.js" import { Func, PropRef, Value } from "../../../src/query/ir.js" import type { NamespacedRow } from "../../../src/types.js" +// Import operators to register evaluators (needed for direct IR testing) +import "../../../src/query/builder/operators/index.js" + describe(`evaluators`, () => { describe(`compileExpression`, () => { it(`handles unknown expression type`, () => { diff --git a/packages/db/tests/query/compiler/select.test.ts b/packages/db/tests/query/compiler/select.test.ts index ab60c7d2a..d2825bf95 100644 --- a/packages/db/tests/query/compiler/select.test.ts +++ b/packages/db/tests/query/compiler/select.test.ts @@ -2,6 +2,9 @@ import { describe, expect, it } from "vitest" import { processArgument } from "../../../src/query/compiler/select.js" import { Aggregate, Func, PropRef, Value } from "../../../src/query/ir.js" +// Import operators to register evaluators (needed for direct IR testing) +import "../../../src/query/builder/operators/index.js" + describe(`select compiler`, () => { // Note: Most of the select compilation logic is tested through the full integration // tests in basic.test.ts and other compiler tests. Here we focus on the standalone From 57b0eebe3cb4ce018b33e09801c6b3794e0ac00f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Dec 2025 22:07:57 +0000 Subject: [PATCH 7/7] 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. --- .changeset/auto-register-operators.md | 46 +++- .../db/src/query/builder/aggregates/avg.ts | 21 +- .../db/src/query/builder/aggregates/count.ts | 17 +- .../db/src/query/builder/aggregates/max.ts | 21 +- .../db/src/query/builder/aggregates/min.ts | 21 +- .../db/src/query/builder/aggregates/sum.ts | 21 +- .../db/src/query/builder/operators/add.ts | 30 +-- .../db/src/query/builder/operators/and.ts | 65 ++--- .../src/query/builder/operators/coalesce.ts | 25 +- .../db/src/query/builder/operators/concat.ts | 29 +-- .../db/src/query/builder/operators/divide.ts | 31 +-- packages/db/src/query/builder/operators/eq.ts | 46 ++-- packages/db/src/query/builder/operators/gt.ts | 44 ++-- .../db/src/query/builder/operators/gte.ts | 44 ++-- .../db/src/query/builder/operators/ilike.ts | 28 +- packages/db/src/query/builder/operators/in.ts | 28 +- .../db/src/query/builder/operators/isNull.ts | 18 +- .../query/builder/operators/isUndefined.ts | 22 +- .../db/src/query/builder/operators/length.ts | 25 +- .../db/src/query/builder/operators/like.ts | 30 +-- .../db/src/query/builder/operators/lower.ts | 25 +- packages/db/src/query/builder/operators/lt.ts | 44 ++-- .../db/src/query/builder/operators/lte.ts | 44 ++-- .../src/query/builder/operators/multiply.ts | 31 +-- .../db/src/query/builder/operators/not.ts | 18 +- packages/db/src/query/builder/operators/or.ts | 65 ++--- .../src/query/builder/operators/subtract.ts | 31 +-- .../db/src/query/builder/operators/upper.ts | 25 +- .../src/query/compiler/aggregate-registry.ts | 57 ---- packages/db/src/query/compiler/evaluators.ts | 21 +- packages/db/src/query/compiler/expressions.ts | 3 +- packages/db/src/query/compiler/group-by.ts | 20 +- packages/db/src/query/compiler/registry.ts | 52 ---- packages/db/src/query/index.ts | 14 +- packages/db/src/query/ir.ts | 37 ++- packages/db/src/query/optimizer.ts | 6 +- packages/db/src/query/predicate-utils.ts | 12 +- .../db/tests/collection-change-events.test.ts | 18 +- .../db/tests/query/compiler/basic.test.ts | 13 +- .../query/compiler/custom-aggregates.test.ts | 155 +++++------ .../query/compiler/custom-operators.test.ts | 165 ++++++------ .../tests/query/compiler/evaluators.test.ts | 246 ++++++++---------- .../db/tests/query/compiler/select.test.ts | 37 +-- 43 files changed, 785 insertions(+), 966 deletions(-) delete mode 100644 packages/db/src/query/compiler/aggregate-registry.ts delete mode 100644 packages/db/src/query/compiler/registry.ts diff --git a/.changeset/auto-register-operators.md b/.changeset/auto-register-operators.md index a18cae698..dc2dae079 100644 --- a/.changeset/auto-register-operators.md +++ b/.changeset/auto-register-operators.md @@ -2,35 +2,53 @@ "@tanstack/db": patch --- -Add auto-registering operators and aggregates for tree-shaking support and custom extensibility. +Refactor operators and aggregates to embed their evaluators directly in IR nodes for true tree-shaking support and custom extensibility. -Each operator and aggregate now bundles its builder function and evaluator in a single file, registering itself when imported. This enables: +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: -- **Tree-shaking**: Only operators/aggregates you import are included in your bundle -- **Custom operators**: Use `registerOperator()` to add your own operators -- **Custom aggregates**: Use `registerAggregate()` to add your own aggregate functions +- **True tree-shaking**: Only operators/aggregates you import are included in your bundle +- **No global registry**: No side-effect imports needed; each node is self-contained +- **Custom operators**: Create custom operators by building `Func` nodes with a factory +- **Custom aggregates**: Create custom aggregates by building `Aggregate` nodes with a config **Custom Operator Example:** ```typescript -import { registerOperator, type EvaluatorFactory } from "@tanstack/db" +import { + Func, + type EvaluatorFactory, + type CompiledExpression, +} from "@tanstack/db" +import { toExpression } from "@tanstack/db/query" -registerOperator("between", (compiledArgs, _isSingleRow) => { +const betweenFactory: EvaluatorFactory = (compiledArgs, _isSingleRow) => { const [valueEval, minEval, maxEval] = compiledArgs return (data) => { const value = valueEval!(data) return value >= minEval!(data) && value <= maxEval!(data) } -}) +} + +function between(value: any, min: any, max: any) { + return new Func( + "between", + [toExpression(value), toExpression(min), toExpression(max)], + betweenFactory + ) +} ``` **Custom Aggregate Example:** ```typescript -import { registerAggregate, type ValueExtractor } from "@tanstack/db" +import { + Aggregate, + type AggregateConfig, + type ValueExtractor, +} from "@tanstack/db" +import { toExpression } from "@tanstack/db/query" -// Custom "product" aggregate that multiplies values -registerAggregate("product", { +const productConfig: AggregateConfig = { factory: (valueExtractor: ValueExtractor) => ({ preMap: valueExtractor, reduce: (values) => { @@ -42,5 +60,9 @@ registerAggregate("product", { }, }), valueTransform: "numeric", -}) +} + +function product(arg: T): Aggregate { + return new Aggregate("product", [toExpression(arg)], productConfig) +} ``` diff --git a/packages/db/src/query/builder/aggregates/avg.ts b/packages/db/src/query/builder/aggregates/avg.ts index e89931b69..b79649dff 100644 --- a/packages/db/src/query/builder/aggregates/avg.ts +++ b/packages/db/src/query/builder/aggregates/avg.ts @@ -1,22 +1,25 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerAggregate } from "../../compiler/aggregate-registry.js" import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ -// BUILDER FUNCTION +// CONFIG // ============================================================ -export function avg(arg: T): AggregateReturnType { - return new Aggregate(`avg`, [toExpression(arg)]) as AggregateReturnType +const avgConfig = { + factory: groupByOperators.avg, + valueTransform: `numeric` as const, } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerAggregate(`avg`, { - factory: groupByOperators.avg, - valueTransform: `numeric`, -}) +export function avg(arg: T): AggregateReturnType { + return new Aggregate( + `avg`, + [toExpression(arg)], + avgConfig + ) as AggregateReturnType +} diff --git a/packages/db/src/query/builder/aggregates/count.ts b/packages/db/src/query/builder/aggregates/count.ts index 561dd7b35..434ac33d6 100644 --- a/packages/db/src/query/builder/aggregates/count.ts +++ b/packages/db/src/query/builder/aggregates/count.ts @@ -1,22 +1,21 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerAggregate } from "../../compiler/aggregate-registry.js" import type { ExpressionLike } from "../operators/types.js" // ============================================================ -// BUILDER FUNCTION +// CONFIG // ============================================================ -export function count(arg: ExpressionLike): Aggregate { - return new Aggregate(`count`, [toExpression(arg)]) +const countConfig = { + factory: groupByOperators.count, + valueTransform: `raw` as const, } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerAggregate(`count`, { - factory: groupByOperators.count, - valueTransform: `raw`, -}) +export function count(arg: ExpressionLike): Aggregate { + return new Aggregate(`count`, [toExpression(arg)], countConfig) +} diff --git a/packages/db/src/query/builder/aggregates/max.ts b/packages/db/src/query/builder/aggregates/max.ts index e9b396c2e..c2790472c 100644 --- a/packages/db/src/query/builder/aggregates/max.ts +++ b/packages/db/src/query/builder/aggregates/max.ts @@ -1,22 +1,25 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerAggregate } from "../../compiler/aggregate-registry.js" import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ -// BUILDER FUNCTION +// CONFIG // ============================================================ -export function max(arg: T): AggregateReturnType { - return new Aggregate(`max`, [toExpression(arg)]) as AggregateReturnType +const maxConfig = { + factory: groupByOperators.max, + valueTransform: `numericOrDate` as const, } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerAggregate(`max`, { - factory: groupByOperators.max, - valueTransform: `numericOrDate`, -}) +export function max(arg: T): AggregateReturnType { + return new Aggregate( + `max`, + [toExpression(arg)], + maxConfig + ) as AggregateReturnType +} diff --git a/packages/db/src/query/builder/aggregates/min.ts b/packages/db/src/query/builder/aggregates/min.ts index f1f3852cd..5d54e72b3 100644 --- a/packages/db/src/query/builder/aggregates/min.ts +++ b/packages/db/src/query/builder/aggregates/min.ts @@ -1,22 +1,25 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerAggregate } from "../../compiler/aggregate-registry.js" import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ -// BUILDER FUNCTION +// CONFIG // ============================================================ -export function min(arg: T): AggregateReturnType { - return new Aggregate(`min`, [toExpression(arg)]) as AggregateReturnType +const minConfig = { + factory: groupByOperators.min, + valueTransform: `numericOrDate` as const, } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerAggregate(`min`, { - factory: groupByOperators.min, - valueTransform: `numericOrDate`, -}) +export function min(arg: T): AggregateReturnType { + return new Aggregate( + `min`, + [toExpression(arg)], + minConfig + ) as AggregateReturnType +} diff --git a/packages/db/src/query/builder/aggregates/sum.ts b/packages/db/src/query/builder/aggregates/sum.ts index 2aa731a1c..52f182afc 100644 --- a/packages/db/src/query/builder/aggregates/sum.ts +++ b/packages/db/src/query/builder/aggregates/sum.ts @@ -1,22 +1,25 @@ import { groupByOperators } from "@tanstack/db-ivm" import { Aggregate } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerAggregate } from "../../compiler/aggregate-registry.js" import type { AggregateReturnType, ExpressionLike } from "../operators/types.js" // ============================================================ -// BUILDER FUNCTION +// CONFIG // ============================================================ -export function sum(arg: T): AggregateReturnType { - return new Aggregate(`sum`, [toExpression(arg)]) as AggregateReturnType +const sumConfig = { + factory: groupByOperators.sum, + valueTransform: `numeric` as const, } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerAggregate(`sum`, { - factory: groupByOperators.sum, - valueTransform: `numeric`, -}) +export function sum(arg: T): AggregateReturnType { + return new Aggregate( + `sum`, + [toExpression(arg)], + sumConfig + ) as AggregateReturnType +} diff --git a/packages/db/src/query/builder/operators/add.ts b/packages/db/src/query/builder/operators/add.ts index 5e639c68b..457522612 100644 --- a/packages/db/src/query/builder/operators/add.ts +++ b/packages/db/src/query/builder/operators/add.ts @@ -1,23 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { CompiledExpression } from "../../ir.js" import type { BinaryNumericReturnType, ExpressionLike } from "./types.js" -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function add( - left: T1, - right: T2 -): BinaryNumericReturnType { - return new Func(`add`, [ - toExpression(left), - toExpression(right), - ]) as BinaryNumericReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -37,7 +22,16 @@ function addEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`add`, addEvaluatorFactory) +export function add( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func( + `add`, + [toExpression(left), toExpression(right)], + addEvaluatorFactory + ) as BinaryNumericReturnType +} diff --git a/packages/db/src/query/builder/operators/and.ts b/packages/db/src/query/builder/operators/and.ts index 5e11eaff3..e347cbc2f 100644 --- a/packages/db/src/query/builder/operators/and.ts +++ b/packages/db/src/query/builder/operators/and.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,32 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -// Overloads for and() - support 2 or more arguments -export function and( - left: ExpressionLike, - right: ExpressionLike -): BasicExpression -export function and( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression -export function and( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression { - const allArgs = [left, right, ...rest] - return new Func( - `and`, - allArgs.map((arg) => toExpression(arg)) - ) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -77,7 +49,38 @@ function andEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`and`, andEvaluatorFactory) +// Overloads for and() - support 2 or more arguments, or an array +export function and( + left: ExpressionLike, + right: ExpressionLike +): BasicExpression +export function and( + left: ExpressionLike, + right: ExpressionLike, + ...rest: Array +): BasicExpression +export function and(args: Array): BasicExpression +export function and( + leftOrArgs: ExpressionLike | Array, + right?: ExpressionLike, + ...rest: Array +): BasicExpression { + // Handle array overload + if (Array.isArray(leftOrArgs) && right === undefined) { + return new Func( + `and`, + leftOrArgs.map((arg) => toExpression(arg)), + andEvaluatorFactory + ) + } + // Handle variadic overload + const allArgs = [leftOrArgs, right!, ...rest] + return new Func( + `and`, + allArgs.map((arg) => toExpression(arg)), + andEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/coalesce.ts b/packages/db/src/query/builder/operators/coalesce.ts index 5e47ae375..51a1cc066 100644 --- a/packages/db/src/query/builder/operators/coalesce.ts +++ b/packages/db/src/query/builder/operators/coalesce.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,17 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function coalesce(...args: Array): BasicExpression { - return new Func( - `coalesce`, - args.map((arg) => toExpression(arg)) - ) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -42,7 +29,13 @@ function coalesceEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`coalesce`, coalesceEvaluatorFactory) +export function coalesce(...args: Array): BasicExpression { + return new Func( + `coalesce`, + args.map((arg) => toExpression(arg)), + coalesceEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/concat.ts b/packages/db/src/query/builder/operators/concat.ts index a3cf8cb26..518e7c486 100644 --- a/packages/db/src/query/builder/operators/concat.ts +++ b/packages/db/src/query/builder/operators/concat.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,19 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function concat( - ...args: Array -): BasicExpression { - return new Func( - `concat`, - args.map((arg) => toExpression(arg)) - ) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -51,7 +36,15 @@ function concatEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`concat`, concatEvaluatorFactory) +export function concat( + ...args: Array +): BasicExpression { + return new Func( + `concat`, + args.map((arg) => toExpression(arg)), + concatEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/divide.ts b/packages/db/src/query/builder/operators/divide.ts index dfdd0cde9..16d20d572 100644 --- a/packages/db/src/query/builder/operators/divide.ts +++ b/packages/db/src/query/builder/operators/divide.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -16,20 +14,6 @@ type BinaryNumericReturnType<_T1, _T2> = BasicExpression< number | undefined | null > -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function divide( - left: T1, - right: T2 -): BinaryNumericReturnType { - return new Func(`divide`, [ - toExpression(left), - toExpression(right), - ]) as BinaryNumericReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -50,7 +34,16 @@ function divideEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`divide`, divideEvaluatorFactory) +export function divide( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func( + `divide`, + [toExpression(left), toExpression(right)], + divideEvaluatorFactory + ) as BinaryNumericReturnType +} diff --git a/packages/db/src/query/builder/operators/eq.ts b/packages/db/src/query/builder/operators/eq.ts index 53bc2b212..e23b1c60f 100644 --- a/packages/db/src/query/builder/operators/eq.ts +++ b/packages/db/src/query/builder/operators/eq.ts @@ -1,11 +1,13 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" import { areValuesEqual, normalizeValue } from "../../../utils/comparison.js" -import type { Aggregate, BasicExpression } from "../../ir.js" +import type { + Aggregate, + BasicExpression, + CompiledExpression, +} from "../../ir.js" import type { RefProxy } from "../ref-proxy.js" import type { RefLeaf } from "../types.js" -import type { CompiledExpression } from "../../compiler/registry.js" // ============================================================ // TYPES @@ -26,24 +28,7 @@ type ComparisonOperandPrimitive = | null // ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function eq( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function eq( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function eq(left: Aggregate, right: any): BasicExpression -export function eq(left: any, right: any): BasicExpression { - return new Func(`eq`, [toExpression(left), toExpression(right)]) -} - -// ============================================================ -// EVALUATOR +// EVALUATOR FACTORY // ============================================================ function isUnknown(value: any): boolean { @@ -71,7 +56,22 @@ function eqEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`eq`, eqEvaluatorFactory) +export function eq( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function eq( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function eq(left: Aggregate, right: any): BasicExpression +export function eq(left: any, right: any): BasicExpression { + return new Func( + `eq`, + [toExpression(left), toExpression(right)], + eqEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/gt.ts b/packages/db/src/query/builder/operators/gt.ts index ca10b283f..7dc83df9b 100644 --- a/packages/db/src/query/builder/operators/gt.ts +++ b/packages/db/src/query/builder/operators/gt.ts @@ -1,10 +1,12 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { Aggregate, BasicExpression } from "../../ir.js" +import type { + Aggregate, + BasicExpression, + CompiledExpression, +} from "../../ir.js" import type { RefProxy } from "../ref-proxy.js" import type { RefLeaf } from "../types.js" -import type { CompiledExpression } from "../../compiler/registry.js" // ============================================================ // TYPES @@ -24,23 +26,6 @@ type ComparisonOperandPrimitive = | undefined | null -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function gt( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function gt( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function gt(left: Aggregate, right: any): BasicExpression -export function gt(left: any, right: any): BasicExpression { - return new Func(`gt`, [toExpression(left), toExpression(right)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -70,7 +55,22 @@ function gtEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`gt`, gtEvaluatorFactory) +export function gt( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function gt( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function gt(left: Aggregate, right: any): BasicExpression +export function gt(left: any, right: any): BasicExpression { + return new Func( + `gt`, + [toExpression(left), toExpression(right)], + gtEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/gte.ts b/packages/db/src/query/builder/operators/gte.ts index fb0b38c62..bd461aea9 100644 --- a/packages/db/src/query/builder/operators/gte.ts +++ b/packages/db/src/query/builder/operators/gte.ts @@ -1,10 +1,12 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { Aggregate, BasicExpression } from "../../ir.js" +import type { + Aggregate, + BasicExpression, + CompiledExpression, +} from "../../ir.js" import type { RefProxy } from "../ref-proxy.js" import type { RefLeaf } from "../types.js" -import type { CompiledExpression } from "../../compiler/registry.js" // ============================================================ // TYPES @@ -24,23 +26,6 @@ type ComparisonOperandPrimitive = | undefined | null -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function gte( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function gte( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function gte(left: Aggregate, right: any): BasicExpression -export function gte(left: any, right: any): BasicExpression { - return new Func(`gte`, [toExpression(left), toExpression(right)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -70,7 +55,22 @@ function gteEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`gte`, gteEvaluatorFactory) +export function gte( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function gte( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function gte(left: Aggregate, right: any): BasicExpression +export function gte(left: any, right: any): BasicExpression { + return new Func( + `gte`, + [toExpression(left), toExpression(right)], + gteEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/ilike.ts b/packages/db/src/query/builder/operators/ilike.ts index 0bd293cde..56d79c636 100644 --- a/packages/db/src/query/builder/operators/ilike.ts +++ b/packages/db/src/query/builder/operators/ilike.ts @@ -1,9 +1,7 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" import { evaluateLike } from "./like.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -15,17 +13,6 @@ type StringRef = | BasicExpression type StringLike = StringRef | string | null | undefined | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function ilike( - left: StringLike, - right: StringLike -): BasicExpression { - return new Func(`ilike`, [toExpression(left), toExpression(right)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -53,7 +40,16 @@ function ilikeEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`ilike`, ilikeEvaluatorFactory) +export function ilike( + left: StringLike, + right: StringLike +): BasicExpression { + return new Func( + `ilike`, + [toExpression(left), toExpression(right)], + ilikeEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/in.ts b/packages/db/src/query/builder/operators/in.ts index 803a013f4..db91f86e1 100644 --- a/packages/db/src/query/builder/operators/in.ts +++ b/packages/db/src/query/builder/operators/in.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,17 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function inArray( - value: ExpressionLike, - array: ExpressionLike -): BasicExpression { - return new Func(`in`, [toExpression(value), toExpression(array)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -52,7 +39,16 @@ function inEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`in`, inEvaluatorFactory) +export function inArray( + value: ExpressionLike, + array: ExpressionLike +): BasicExpression { + return new Func( + `in`, + [toExpression(value), toExpression(array)], + inEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/isNull.ts b/packages/db/src/query/builder/operators/isNull.ts index 170ba9bf2..d4ea771eb 100644 --- a/packages/db/src/query/builder/operators/isNull.ts +++ b/packages/db/src/query/builder/operators/isNull.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,14 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function isNull(value: ExpressionLike): BasicExpression { - return new Func(`isNull`, [toExpression(value)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -36,7 +26,9 @@ function isNullEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`isNull`, isNullEvaluatorFactory) +export function isNull(value: ExpressionLike): BasicExpression { + return new Func(`isNull`, [toExpression(value)], isNullEvaluatorFactory) +} diff --git a/packages/db/src/query/builder/operators/isUndefined.ts b/packages/db/src/query/builder/operators/isUndefined.ts index ab7c804ab..4598c3054 100644 --- a/packages/db/src/query/builder/operators/isUndefined.ts +++ b/packages/db/src/query/builder/operators/isUndefined.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,14 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function isUndefined(value: ExpressionLike): BasicExpression { - return new Func(`isUndefined`, [toExpression(value)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -36,7 +26,13 @@ function isUndefinedEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`isUndefined`, isUndefinedEvaluatorFactory) +export function isUndefined(value: ExpressionLike): BasicExpression { + return new Func( + `isUndefined`, + [toExpression(value)], + isUndefinedEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/length.ts b/packages/db/src/query/builder/operators/length.ts index 9f34ddd1d..597b738c8 100644 --- a/packages/db/src/query/builder/operators/length.ts +++ b/packages/db/src/query/builder/operators/length.ts @@ -1,19 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { CompiledExpression } from "../../ir.js" import type { ExpressionLike, NumericFunctionReturnType } from "./types.js" -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function length( - arg: T -): NumericFunctionReturnType { - return new Func(`length`, [toExpression(arg)]) as NumericFunctionReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -37,7 +26,15 @@ function lengthEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`length`, lengthEvaluatorFactory) +export function length( + arg: T +): NumericFunctionReturnType { + return new Func( + `length`, + [toExpression(arg)], + lengthEvaluatorFactory + ) as NumericFunctionReturnType +} diff --git a/packages/db/src/query/builder/operators/like.ts b/packages/db/src/query/builder/operators/like.ts index 80b365180..b9763b8e2 100644 --- a/packages/db/src/query/builder/operators/like.ts +++ b/packages/db/src/query/builder/operators/like.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -14,18 +12,6 @@ type StringRef = | BasicExpression type StringLike = StringRef | string | null | undefined | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function like( - left: StringLike, - right: StringLike -): BasicExpression -export function like(left: any, right: any): BasicExpression { - return new Func(`like`, [toExpression(left), toExpression(right)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -80,10 +66,20 @@ function likeEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`like`, likeEvaluatorFactory) +export function like( + left: StringLike, + right: StringLike +): BasicExpression +export function like(left: any, right: any): BasicExpression { + return new Func( + `like`, + [toExpression(left), toExpression(right)], + likeEvaluatorFactory + ) +} // Export for use by ilike export { evaluateLike } diff --git a/packages/db/src/query/builder/operators/lower.ts b/packages/db/src/query/builder/operators/lower.ts index f67a787c4..614980ec0 100644 --- a/packages/db/src/query/builder/operators/lower.ts +++ b/packages/db/src/query/builder/operators/lower.ts @@ -1,19 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { CompiledExpression } from "../../ir.js" import type { ExpressionLike, StringFunctionReturnType } from "./types.js" -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function lower( - arg: T -): StringFunctionReturnType { - return new Func(`lower`, [toExpression(arg)]) as StringFunctionReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -31,7 +20,15 @@ function lowerEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`lower`, lowerEvaluatorFactory) +export function lower( + arg: T +): StringFunctionReturnType { + return new Func( + `lower`, + [toExpression(arg)], + lowerEvaluatorFactory + ) as StringFunctionReturnType +} diff --git a/packages/db/src/query/builder/operators/lt.ts b/packages/db/src/query/builder/operators/lt.ts index f7401c213..f84c0b4d3 100644 --- a/packages/db/src/query/builder/operators/lt.ts +++ b/packages/db/src/query/builder/operators/lt.ts @@ -1,10 +1,12 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { Aggregate, BasicExpression } from "../../ir.js" +import type { + Aggregate, + BasicExpression, + CompiledExpression, +} from "../../ir.js" import type { RefProxy } from "../ref-proxy.js" import type { RefLeaf } from "../types.js" -import type { CompiledExpression } from "../../compiler/registry.js" // ============================================================ // TYPES @@ -24,23 +26,6 @@ type ComparisonOperandPrimitive = | undefined | null -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function lt( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function lt( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function lt(left: Aggregate, right: any): BasicExpression -export function lt(left: any, right: any): BasicExpression { - return new Func(`lt`, [toExpression(left), toExpression(right)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -70,7 +55,22 @@ function ltEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`lt`, ltEvaluatorFactory) +export function lt( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function lt( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function lt(left: Aggregate, right: any): BasicExpression +export function lt(left: any, right: any): BasicExpression { + return new Func( + `lt`, + [toExpression(left), toExpression(right)], + ltEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/lte.ts b/packages/db/src/query/builder/operators/lte.ts index eb40f6d7b..9ebad0938 100644 --- a/packages/db/src/query/builder/operators/lte.ts +++ b/packages/db/src/query/builder/operators/lte.ts @@ -1,10 +1,12 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { Aggregate, BasicExpression } from "../../ir.js" +import type { + Aggregate, + BasicExpression, + CompiledExpression, +} from "../../ir.js" import type { RefProxy } from "../ref-proxy.js" import type { RefLeaf } from "../types.js" -import type { CompiledExpression } from "../../compiler/registry.js" // ============================================================ // TYPES @@ -24,23 +26,6 @@ type ComparisonOperandPrimitive = | undefined | null -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function lte( - left: ComparisonOperand, - right: ComparisonOperand -): BasicExpression -export function lte( - left: ComparisonOperandPrimitive, - right: ComparisonOperandPrimitive -): BasicExpression -export function lte(left: Aggregate, right: any): BasicExpression -export function lte(left: any, right: any): BasicExpression { - return new Func(`lte`, [toExpression(left), toExpression(right)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -70,7 +55,22 @@ function lteEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`lte`, lteEvaluatorFactory) +export function lte( + left: ComparisonOperand, + right: ComparisonOperand +): BasicExpression +export function lte( + left: ComparisonOperandPrimitive, + right: ComparisonOperandPrimitive +): BasicExpression +export function lte(left: Aggregate, right: any): BasicExpression +export function lte(left: any, right: any): BasicExpression { + return new Func( + `lte`, + [toExpression(left), toExpression(right)], + lteEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/multiply.ts b/packages/db/src/query/builder/operators/multiply.ts index d554689a4..cf446d6d0 100644 --- a/packages/db/src/query/builder/operators/multiply.ts +++ b/packages/db/src/query/builder/operators/multiply.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -16,20 +14,6 @@ type BinaryNumericReturnType<_T1, _T2> = BasicExpression< number | undefined | null > -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function multiply( - left: T1, - right: T2 -): BinaryNumericReturnType { - return new Func(`multiply`, [ - toExpression(left), - toExpression(right), - ]) as BinaryNumericReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -49,7 +33,16 @@ function multiplyEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`multiply`, multiplyEvaluatorFactory) +export function multiply( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func( + `multiply`, + [toExpression(left), toExpression(right)], + multiplyEvaluatorFactory + ) as BinaryNumericReturnType +} diff --git a/packages/db/src/query/builder/operators/not.ts b/packages/db/src/query/builder/operators/not.ts index 77d30edb3..8e2598bdf 100644 --- a/packages/db/src/query/builder/operators/not.ts +++ b/packages/db/src/query/builder/operators/not.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,14 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function not(value: ExpressionLike): BasicExpression { - return new Func(`not`, [toExpression(value)]) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -47,7 +37,9 @@ function notEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`not`, notEvaluatorFactory) +export function not(value: ExpressionLike): BasicExpression { + return new Func(`not`, [toExpression(value)], notEvaluatorFactory) +} diff --git a/packages/db/src/query/builder/operators/or.ts b/packages/db/src/query/builder/operators/or.ts index 80a79b26b..8727f75a3 100644 --- a/packages/db/src/query/builder/operators/or.ts +++ b/packages/db/src/query/builder/operators/or.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -11,32 +9,6 @@ import type { CompiledExpression } from "../../compiler/registry.js" // Helper type for any expression-like value type ExpressionLike = BasicExpression | any -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -// Overloads for or() - support 2 or more arguments -export function or( - left: ExpressionLike, - right: ExpressionLike -): BasicExpression -export function or( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression -export function or( - left: ExpressionLike, - right: ExpressionLike, - ...rest: Array -): BasicExpression { - const allArgs = [left, right, ...rest] - return new Func( - `or`, - allArgs.map((arg) => toExpression(arg)) - ) -} - // ============================================================ // EVALUATOR // ============================================================ @@ -75,7 +47,38 @@ function orEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`or`, orEvaluatorFactory) +// Overloads for or() - support 2 or more arguments, or an array +export function or( + left: ExpressionLike, + right: ExpressionLike +): BasicExpression +export function or( + left: ExpressionLike, + right: ExpressionLike, + ...rest: Array +): BasicExpression +export function or(args: Array): BasicExpression +export function or( + leftOrArgs: ExpressionLike | Array, + right?: ExpressionLike, + ...rest: Array +): BasicExpression { + // Handle array overload + if (Array.isArray(leftOrArgs) && right === undefined) { + return new Func( + `or`, + leftOrArgs.map((arg) => toExpression(arg)), + orEvaluatorFactory + ) + } + // Handle variadic overload + const allArgs = [leftOrArgs, right!, ...rest] + return new Func( + `or`, + allArgs.map((arg) => toExpression(arg)), + orEvaluatorFactory + ) +} diff --git a/packages/db/src/query/builder/operators/subtract.ts b/packages/db/src/query/builder/operators/subtract.ts index d69bdfa33..21fb5372d 100644 --- a/packages/db/src/query/builder/operators/subtract.ts +++ b/packages/db/src/query/builder/operators/subtract.ts @@ -1,8 +1,6 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { BasicExpression } from "../../ir.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { BasicExpression, CompiledExpression } from "../../ir.js" // ============================================================ // TYPES @@ -16,20 +14,6 @@ type BinaryNumericReturnType<_T1, _T2> = BasicExpression< number | undefined | null > -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function subtract( - left: T1, - right: T2 -): BinaryNumericReturnType { - return new Func(`subtract`, [ - toExpression(left), - toExpression(right), - ]) as BinaryNumericReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -49,7 +33,16 @@ function subtractEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`subtract`, subtractEvaluatorFactory) +export function subtract( + left: T1, + right: T2 +): BinaryNumericReturnType { + return new Func( + `subtract`, + [toExpression(left), toExpression(right)], + subtractEvaluatorFactory + ) as BinaryNumericReturnType +} diff --git a/packages/db/src/query/builder/operators/upper.ts b/packages/db/src/query/builder/operators/upper.ts index 7090be19f..7c79ea92a 100644 --- a/packages/db/src/query/builder/operators/upper.ts +++ b/packages/db/src/query/builder/operators/upper.ts @@ -1,19 +1,8 @@ import { Func } from "../../ir.js" import { toExpression } from "../ref-proxy.js" -import { registerOperator } from "../../compiler/registry.js" -import type { CompiledExpression } from "../../compiler/registry.js" +import type { CompiledExpression } from "../../ir.js" import type { ExpressionLike, StringFunctionReturnType } from "./types.js" -// ============================================================ -// BUILDER FUNCTION -// ============================================================ - -export function upper( - arg: T -): StringFunctionReturnType { - return new Func(`upper`, [toExpression(arg)]) as StringFunctionReturnType -} - // ============================================================ // EVALUATOR // ============================================================ @@ -31,7 +20,15 @@ function upperEvaluatorFactory( } // ============================================================ -// AUTO-REGISTRATION +// BUILDER FUNCTION // ============================================================ -registerOperator(`upper`, upperEvaluatorFactory) +export function upper( + arg: T +): StringFunctionReturnType { + return new Func( + `upper`, + [toExpression(arg)], + upperEvaluatorFactory + ) as StringFunctionReturnType +} diff --git a/packages/db/src/query/compiler/aggregate-registry.ts b/packages/db/src/query/compiler/aggregate-registry.ts deleted file mode 100644 index 227a4c471..000000000 --- a/packages/db/src/query/compiler/aggregate-registry.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { UnsupportedAggregateFunctionError } from "../../errors.js" -import type { NamespacedRow } from "../../types.js" - -/** - * Value extractor type - extracts a value from a namespaced row - */ -export type ValueExtractor = (entry: [string, NamespacedRow]) => any - -/** - * Aggregate function factory - creates an IVM aggregate from a value extractor - */ -export type AggregateFactory = (valueExtractor: ValueExtractor) => any - -/** - * Configuration for how to create a value extractor for this aggregate - */ -export interface AggregateConfig { - /** The IVM aggregate function factory */ - factory: AggregateFactory - /** How to transform the compiled expression value */ - valueTransform: `numeric` | `numericOrDate` | `raw` -} - -/** - * Registry mapping aggregate names to their configurations - */ -const aggregateRegistry = new Map() - -/** - * Register an aggregate function. - * Called automatically when an aggregate module is imported. - */ -export function registerAggregate(name: string, config: AggregateConfig): void { - aggregateRegistry.set(name.toLowerCase(), config) -} - -/** - * Get an aggregate's configuration. - * Throws if the aggregate hasn't been registered. - */ -export function getAggregateConfig(name: string): AggregateConfig { - const config = aggregateRegistry.get(name.toLowerCase()) - if (!config) { - throw new UnsupportedAggregateFunctionError(name) - } - return config -} - -/** - * Try to get an aggregate's configuration. - * Returns undefined if the aggregate hasn't been registered. - */ -export function tryGetAggregateConfig( - name: string -): AggregateConfig | undefined { - return aggregateRegistry.get(name.toLowerCase()) -} diff --git a/packages/db/src/query/compiler/evaluators.ts b/packages/db/src/query/compiler/evaluators.ts index 97e138d38..f98701780 100644 --- a/packages/db/src/query/compiler/evaluators.ts +++ b/packages/db/src/query/compiler/evaluators.ts @@ -3,14 +3,11 @@ import { UnknownExpressionTypeError, UnknownFunctionError, } from "../../errors.js" -import { tryGetOperatorEvaluator } from "./registry.js" import type { BasicExpression, Func, PropRef } from "../ir.js" import type { NamespacedRow } from "../../types.js" -// Operators are lazily registered when imported by user code. -// Each operator file (e.g., eq.ts) auto-registers its evaluator on import. -// If a user uses an operator without importing it, compilation will fail -// with an UnknownFunctionError guiding them to import it. +// Each operator's Func node carries its own factory function. +// No global registry is needed - the factory is passed directly to Func. /** * Converts a 3-valued logic result to a boolean for use in WHERE/HAVING filters. @@ -159,15 +156,11 @@ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { compileExpressionInternal(arg, isSingleRow) ) - // Try registry first (for migrated operators) - const evaluatorFactory = tryGetOperatorEvaluator(func.name) - if (evaluatorFactory) { - return evaluatorFactory(compiledArgs, isSingleRow) + // Use the factory embedded in the Func node + if (func.factory) { + return func.factory(compiledArgs, isSingleRow) } - // Fall back to switch for non-migrated operators (currently none, but kept for extensibility) - switch (func.name) { - default: - throw new UnknownFunctionError(func.name) - } + // No factory available - the operator wasn't imported + throw new UnknownFunctionError(func.name) } diff --git a/packages/db/src/query/compiler/expressions.ts b/packages/db/src/query/compiler/expressions.ts index ad7e4f317..abaeeb116 100644 --- a/packages/db/src/query/compiler/expressions.ts +++ b/packages/db/src/query/compiler/expressions.ts @@ -48,7 +48,8 @@ export function normalizeExpressionPaths( ) args.push(convertedArg) } - return new Func(whereClause.name, args) + // Preserve the factory from the original Func + return new Func(whereClause.name, args, whereClause.factory) } } diff --git a/packages/db/src/query/compiler/group-by.ts b/packages/db/src/query/compiler/group-by.ts index 192bd43ad..c7e3296a7 100644 --- a/packages/db/src/query/compiler/group-by.ts +++ b/packages/db/src/query/compiler/group-by.ts @@ -4,9 +4,9 @@ import { AggregateFunctionNotInSelectError, NonAggregateExpressionNotInGroupByError, UnknownHavingExpressionTypeError, + UnsupportedAggregateFunctionError, } from "../../errors.js" import { compileExpression, toBooleanPredicate } from "./evaluators.js" -import { getAggregateConfig } from "./aggregate-registry.js" import type { Aggregate, BasicExpression, @@ -16,10 +16,8 @@ import type { } from "../ir.js" import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js" -// Aggregates are lazily registered when imported by user code. -// Each aggregate file (e.g., sum.ts) auto-registers its config on import. -// If a user uses an aggregate without importing it, compilation will fail -// with an UnsupportedAggregateFunctionError guiding them to import it. +// Each aggregate's Aggregate node carries its own config. +// No global registry is needed - the config is passed directly to Aggregate. /** * Interface for caching the mapping between GROUP BY expressions and SELECT expressions @@ -345,8 +343,11 @@ function getAggregateFunction(aggExpr: Aggregate) { // Pre-compile the value extractor expression const compiledExpr = compileExpression(aggExpr.args[0]!) - // Get the aggregate configuration from registry - const config = getAggregateConfig(aggExpr.name) + // Use the config embedded in the Aggregate node + const config = aggExpr.config + if (!config) { + throw new UnsupportedAggregateFunctionError(aggExpr.name) + } // Create the appropriate value extractor based on the config let valueExtractor: (entry: [string, NamespacedRow]) => any @@ -381,7 +382,7 @@ function getAggregateFunction(aggExpr: Aggregate) { break } - // Return the aggregate function using the registered factory + // Return the aggregate function using the embedded factory return config.factory(valueExtractor) } @@ -414,7 +415,8 @@ export function replaceAggregatesByRefs( (arg: BasicExpression | Aggregate) => replaceAggregatesByRefs(arg, selectClause) ) - return new Func(funcExpr.name, transformedArgs) + // Preserve the factory from the original Func + return new Func(funcExpr.name, transformedArgs, funcExpr.factory) } case `ref`: { diff --git a/packages/db/src/query/compiler/registry.ts b/packages/db/src/query/compiler/registry.ts deleted file mode 100644 index 40bb21670..000000000 --- a/packages/db/src/query/compiler/registry.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { UnknownFunctionError } from "../../errors.js" - -/** - * Type for a compiled expression evaluator - */ -export type CompiledExpression = (data: any) => any - -/** - * Factory function that creates an evaluator from compiled arguments - */ -export type EvaluatorFactory = ( - compiledArgs: Array, - isSingleRow: boolean -) => CompiledExpression - -/** - * Registry mapping operator names to their evaluator factories - */ -const operatorRegistry = new Map() - -/** - * Register an operator's evaluator factory. - * Called automatically when an operator module is imported. - */ -export function registerOperator( - name: string, - factory: EvaluatorFactory -): void { - operatorRegistry.set(name, factory) -} - -/** - * Get an operator's evaluator factory. - * Throws if the operator hasn't been registered. - */ -export function getOperatorEvaluator(name: string): EvaluatorFactory { - const factory = operatorRegistry.get(name) - if (!factory) { - throw new UnknownFunctionError(name) - } - return factory -} - -/** - * Try to get an operator's evaluator factory. - * Returns undefined if the operator hasn't been registered. - */ -export function tryGetOperatorEvaluator( - name: string -): EvaluatorFactory | undefined { - return operatorRegistry.get(name) -} diff --git a/packages/db/src/query/index.ts b/packages/db/src/query/index.ts index 13c38e552..68be7df47 100644 --- a/packages/db/src/query/index.ts +++ b/packages/db/src/query/index.ts @@ -47,20 +47,18 @@ export { // Aggregates - now from aggregate modules for tree-shaking export { count, avg, sum, min, max } from "./builder/aggregates/index.js" -// Operator registry for custom operators +// Types for custom operators and aggregates +// Custom operators: create a Func with your own factory as the 3rd argument +// Custom aggregates: create an Aggregate with your own config as the 3rd argument export { - registerOperator, + Func, + Aggregate, type EvaluatorFactory, type CompiledExpression, -} from "./compiler/registry.js" - -// Aggregate registry for custom aggregates -export { - registerAggregate, type AggregateConfig, type AggregateFactory, type ValueExtractor, -} from "./compiler/aggregate-registry.js" +} from "./ir.js" // Ref proxy utilities export type { Ref } from "./builder/types.js" diff --git a/packages/db/src/query/ir.ts b/packages/db/src/query/ir.ts index d493aaa64..6b4478131 100644 --- a/packages/db/src/query/ir.ts +++ b/packages/db/src/query/ir.ts @@ -6,6 +6,37 @@ import type { CompareOptions } from "./builder/types" import type { Collection, CollectionImpl } from "../collection/index.js" import type { NamespacedRow } from "../types" +/** + * Type for a compiled expression evaluator + */ +export type CompiledExpression = (data: any) => any + +/** + * Factory function that creates an evaluator from compiled arguments + */ +export type EvaluatorFactory = ( + compiledArgs: Array, + isSingleRow: boolean +) => CompiledExpression + +/** + * Value extractor for aggregate functions + */ +export type ValueExtractor = (entry: [string, NamespacedRow]) => any + +/** + * Factory function that creates an aggregate from a value extractor + */ +export type AggregateFactory = (valueExtractor: ValueExtractor) => any + +/** + * Configuration for an aggregate function + */ +export interface AggregateConfig { + factory: AggregateFactory + valueTransform: `numeric` | `numericOrDate` | `raw` +} + export interface QueryIR { from: From select?: Select @@ -111,7 +142,8 @@ export class Func extends BaseExpression { public type = `func` as const constructor( public name: string, // such as eq, gt, lt, upper, lower, etc. - public args: Array + public args: Array, + public factory?: EvaluatorFactory // optional: the evaluator factory for this function ) { super() } @@ -126,7 +158,8 @@ export class Aggregate extends BaseExpression { public type = `agg` as const constructor( public name: string, // such as count, avg, sum, min, max, etc. - public args: Array + public args: Array, + public config?: AggregateConfig // optional: the aggregate configuration ) { super() } diff --git a/packages/db/src/query/optimizer.ts b/packages/db/src/query/optimizer.ts index b5ff6370a..5110bc9b9 100644 --- a/packages/db/src/query/optimizer.ts +++ b/packages/db/src/query/optimizer.ts @@ -124,13 +124,13 @@ import { deepEquals } from "../utils.js" import { CannotCombineEmptyExpressionListError } from "../errors.js" import { CollectionRef as CollectionRefClass, - Func, PropRef, QueryRef as QueryRefClass, createResidualWhere, getWhereExpression, isResidualWhere, } from "./ir.js" +import { and as andBuilder } from "./builder/operators/and.js" import type { BasicExpression, From, QueryIR, Select, Where } from "./ir.js" /** @@ -1056,6 +1056,6 @@ function combineWithAnd( return expressions[0]! } - // Create an AND function with all expressions as arguments - return new Func(`and`, expressions) + // Use the builder function to create an AND with the proper evaluator factory + return andBuilder(expressions) } diff --git a/packages/db/src/query/predicate-utils.ts b/packages/db/src/query/predicate-utils.ts index 3ab7434d2..b981b64ff 100644 --- a/packages/db/src/query/predicate-utils.ts +++ b/packages/db/src/query/predicate-utils.ts @@ -1,5 +1,7 @@ -import { Func, Value } from "./ir.js" -import type { BasicExpression, OrderBy, PropRef } from "./ir.js" +import { Value } from "./ir.js" +import { or as orBuilder } from "./builder/operators/or.js" +import { eq as eqBuilder } from "./builder/operators/eq.js" +import type { BasicExpression, Func, OrderBy, PropRef } from "./ir.js" import type { LoadSubsetOptions } from "../types.js" /** @@ -52,12 +54,14 @@ function makeDisjunction( if (preds.length === 1) { return preds[0]! } - return new Func(`or`, preds) + // Use the builder function to create an OR with the proper evaluator factory + return orBuilder(preds) } function convertInToOr(inField: InField) { const equalities = inField.values.map( - (value) => new Func(`eq`, [inField.ref, new Value(value)]) + // Use the builder function to create EQ with the proper evaluator factory + (value) => eqBuilder(inField.ref, new Value(value)) ) return makeDisjunction(equalities) } diff --git a/packages/db/tests/collection-change-events.test.ts b/packages/db/tests/collection-change-events.test.ts index 25b7dbb1f..1a1c35db0 100644 --- a/packages/db/tests/collection-change-events.test.ts +++ b/packages/db/tests/collection-change-events.test.ts @@ -1,8 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { createCollection } from "../src/collection/index.js" import { currentStateAsChanges } from "../src/collection/change-events.js" -import { Func, PropRef, Value } from "../src/query/ir.js" +import { PropRef, Value } from "../src/query/ir.js" import { DEFAULT_COMPARE_OPTIONS } from "../src/utils.js" +import { eq, gt } from "../src/query/builder/operators/index.js" interface TestUser { id: string @@ -90,7 +91,7 @@ describe(`currentStateAsChanges`, () => { ) const result = currentStateAsChanges(collection, { - where: new Func(`eq`, [new PropRef([`status`]), new Value(`active`)]), + where: eq(new PropRef([`status`]), new Value(`active`)), }) expect(result).toHaveLength(3) @@ -107,7 +108,7 @@ describe(`currentStateAsChanges`, () => { ) const result = currentStateAsChanges(collection, { - where: new Func(`gt`, [new PropRef([`age`]), new Value(25)]), + where: gt(new PropRef([`age`]), new Value(25)), }) expect(result).toHaveLength(3) @@ -225,7 +226,7 @@ describe(`currentStateAsChanges`, () => { ) const result = currentStateAsChanges(collection, { - where: new Func(`eq`, [new PropRef([`status`]), new Value(`active`)]), + where: eq(new PropRef([`status`]), new Value(`active`)), orderBy: [ { expression: new PropRef([`score`]), @@ -248,7 +249,7 @@ describe(`currentStateAsChanges`, () => { ) const result = currentStateAsChanges(collection, { - where: new Func(`gt`, [new PropRef([`age`]), new Value(25)]), + where: gt(new PropRef([`age`]), new Value(25)), orderBy: [ { expression: new PropRef([`age`]), @@ -271,7 +272,7 @@ describe(`currentStateAsChanges`, () => { ) const result = currentStateAsChanges(collection, { - where: new Func(`eq`, [new PropRef([`status`]), new Value(`active`)]), + where: eq(new PropRef([`status`]), new Value(`active`)), orderBy: [ { expression: new PropRef([`score`]), @@ -314,10 +315,7 @@ describe(`currentStateAsChanges`, () => { expect(() => { currentStateAsChanges(collection, { - where: new Func(`eq`, [ - new PropRef([`status`]), - new Value(`active`), - ]), + where: eq(new PropRef([`status`]), new Value(`active`)), limit: 3, }) }).toThrow(`limit cannot be used without orderBy`) diff --git a/packages/db/tests/query/compiler/basic.test.ts b/packages/db/tests/query/compiler/basic.test.ts index 32c548d8e..e7b687b37 100644 --- a/packages/db/tests/query/compiler/basic.test.ts +++ b/packages/db/tests/query/compiler/basic.test.ts @@ -1,7 +1,8 @@ import { describe, expect, test } from "vitest" import { D2, MultiSet, output } from "@tanstack/db-ivm" import { compileQuery } from "../../../src/query/compiler/index.js" -import { CollectionRef, Func, PropRef, Value } from "../../../src/query/ir.js" +import { CollectionRef, PropRef, Value } from "../../../src/query/ir.js" +import { and, eq, gt } from "../../../src/query/builder/operators/index.js" import type { QueryIR } from "../../../src/query/ir.js" import type { CollectionImpl } from "../../../src/collection/index.js" @@ -172,7 +173,7 @@ describe(`Query2 Compiler`, () => { name: new PropRef([`users`, `name`]), age: new PropRef([`users`, `age`]), }, - where: [new Func(`gt`, [new PropRef([`users`, `age`]), new Value(20)])], + where: [gt(new PropRef([`users`, `age`]), new Value(20))], } const graph = new D2() @@ -234,10 +235,10 @@ describe(`Query2 Compiler`, () => { name: new PropRef([`users`, `name`]), }, where: [ - new Func(`and`, [ - new Func(`gt`, [new PropRef([`users`, `age`]), new Value(20)]), - new Func(`eq`, [new PropRef([`users`, `active`]), new Value(true)]), - ]), + and( + gt(new PropRef([`users`, `age`]), new Value(20)), + eq(new PropRef([`users`, `active`]), new Value(true)) + ), ], } diff --git a/packages/db/tests/query/compiler/custom-aggregates.test.ts b/packages/db/tests/query/compiler/custom-aggregates.test.ts index a2ece2f32..cd180e2b6 100644 --- a/packages/db/tests/query/compiler/custom-aggregates.test.ts +++ b/packages/db/tests/query/compiler/custom-aggregates.test.ts @@ -1,19 +1,18 @@ -import { beforeAll, describe, expect, it } from "vitest" +import { describe, expect, it } from "vitest" import { createCollection } from "../../../src/collection/index.js" import { + Aggregate, avg, count, createLiveQueryCollection, sum, } from "../../../src/query/index.js" -import { Aggregate } from "../../../src/query/ir.js" import { toExpression } from "../../../src/query/builder/ref-proxy.js" -import { - getAggregateConfig, - registerAggregate, -} from "../../../src/query/compiler/aggregate-registry.js" import { mockSyncCollectionOptions } from "../../utils.js" -import type { ValueExtractor } from "../../../src/query/compiler/aggregate-registry.js" +import type { + AggregateConfig, + ValueExtractor, +} from "../../../src/query/index.js" interface TestItem { id: number @@ -39,106 +38,84 @@ function createTestCollection() { ) } -// Custom aggregate builder function (follows the same pattern as sum, count, etc.) +// Custom aggregate configs (following the same pattern as built-in aggregates) +const productConfig: AggregateConfig = { + factory: (valueExtractor: ValueExtractor) => ({ + preMap: valueExtractor, + reduce: (values: Array<[number, number]>) => { + let result = 1 + for (const [value, multiplicity] of values) { + // For positive multiplicity, multiply the value that many times + // For negative multiplicity, divide (inverse operation for IVM) + if (multiplicity > 0) { + for (let i = 0; i < multiplicity; i++) { + result *= value + } + } else if (multiplicity < 0) { + for (let i = 0; i < -multiplicity; i++) { + result /= value + } + } + } + return result + }, + }), + valueTransform: `numeric`, +} + +const varianceConfig: AggregateConfig = { + factory: (valueExtractor: ValueExtractor) => ({ + preMap: (data: any) => { + const value = valueExtractor(data) + return { sum: value, sumSq: value * value, n: 1 } + }, + reduce: ( + values: Array<[{ sum: number; sumSq: number; n: number }, number]> + ) => { + let totalSum = 0 + let totalSumSq = 0 + let totalN = 0 + for (const [{ sum: s, sumSq, n }, multiplicity] of values) { + totalSum += s * multiplicity + totalSumSq += sumSq * multiplicity + totalN += n * multiplicity + } + return { sum: totalSum, sumSq: totalSumSq, n: totalN } + }, + postMap: (acc: { sum: number; sumSq: number; n: number }) => { + if (acc.n === 0) return 0 + const mean = acc.sum / acc.n + return acc.sumSq / acc.n - mean * mean + }, + }), + valueTransform: `raw`, // We handle the transformation in preMap +} + +// Custom aggregate builder functions (pass config as 3rd argument to Aggregate) function product(arg: T): Aggregate { - return new Aggregate(`product`, [toExpression(arg)]) + return new Aggregate(`product`, [toExpression(arg)], productConfig) } function variance(arg: T): Aggregate { - return new Aggregate(`variance`, [toExpression(arg)]) + return new Aggregate(`variance`, [toExpression(arg)], varianceConfig) } describe(`Custom Aggregates`, () => { - beforeAll(() => { - // Register custom aggregates for testing - // Aggregate functions must implement the IVM aggregate interface: - // { preMap: (data) => V, reduce: (values: [V, multiplicity][]) => V, postMap?: (V) => R } - - // Custom product aggregate: multiplies all values together - registerAggregate(`product`, { - factory: (valueExtractor: ValueExtractor) => ({ - preMap: valueExtractor, - reduce: (values: Array<[number, number]>) => { - let product = 1 - for (const [value, multiplicity] of values) { - // For positive multiplicity, multiply the value that many times - // For negative multiplicity, divide (inverse operation for IVM) - if (multiplicity > 0) { - for (let i = 0; i < multiplicity; i++) { - product *= value - } - } else if (multiplicity < 0) { - for (let i = 0; i < -multiplicity; i++) { - product /= value - } - } - } - return product - }, - }), - valueTransform: `numeric`, - }) - - // Custom variance aggregate (simplified - population variance) - // Stores { sum, sumSq, n } to compute variance - registerAggregate(`variance`, { - factory: (valueExtractor: ValueExtractor) => ({ - preMap: (data: any) => { - const value = valueExtractor(data) - return { sum: value, sumSq: value * value, n: 1 } - }, - reduce: ( - values: Array<[{ sum: number; sumSq: number; n: number }, number]> - ) => { - let totalSum = 0 - let totalSumSq = 0 - let totalN = 0 - for (const [{ sum, sumSq, n }, multiplicity] of values) { - totalSum += sum * multiplicity - totalSumSq += sumSq * multiplicity - totalN += n * multiplicity - } - return { sum: totalSum, sumSq: totalSumSq, n: totalN } - }, - postMap: (acc: { sum: number; sumSq: number; n: number }) => { - if (acc.n === 0) return 0 - const mean = acc.sum / acc.n - return acc.sumSq / acc.n - mean * mean - }, - }), - valueTransform: `raw`, // We handle the transformation in preMap - }) - }) - - describe(`registerAggregate`, () => { - it(`registers a custom aggregate in the registry`, () => { - const config = getAggregateConfig(`product`) - expect(config).toBeDefined() - expect(config.valueTransform).toBe(`numeric`) - expect(typeof config.factory).toBe(`function`) - }) - - it(`retrieves custom aggregate config (case-insensitive)`, () => { - const config1 = getAggregateConfig(`Product`) - const config2 = getAggregateConfig(`PRODUCT`) - expect(config1).toBeDefined() - expect(config2).toBeDefined() - }) - }) - describe(`custom aggregate builder functions`, () => { - it(`creates an Aggregate IR node for product`, () => { + it(`creates an Aggregate IR node for product with embedded config`, () => { const agg = product(10) expect(agg.type).toBe(`agg`) expect(agg.name).toBe(`product`) expect(agg.args).toHaveLength(1) + expect(agg.config).toBe(productConfig) }) - it(`creates an Aggregate IR node for variance`, () => { + it(`creates an Aggregate IR node for variance with embedded config`, () => { const agg = variance(10) expect(agg.type).toBe(`agg`) expect(agg.name).toBe(`variance`) expect(agg.args).toHaveLength(1) + expect(agg.config).toBe(varianceConfig) }) }) diff --git a/packages/db/tests/query/compiler/custom-operators.test.ts b/packages/db/tests/query/compiler/custom-operators.test.ts index fe3988d84..d39d91cab 100644 --- a/packages/db/tests/query/compiler/custom-operators.test.ts +++ b/packages/db/tests/query/compiler/custom-operators.test.ts @@ -1,78 +1,70 @@ import { describe, expect, it } from "vitest" import { compileExpression } from "../../../src/query/compiler/evaluators.js" -import { registerOperator } from "../../../src/query/compiler/registry.js" import { Func, PropRef, Value } from "../../../src/query/ir.js" import { toExpression } from "../../../src/query/builder/ref-proxy.js" +import { and } from "../../../src/query/builder/operators/index.js" import type { + BasicExpression, CompiledExpression, EvaluatorFactory, -} from "../../../src/query/compiler/registry.js" -import type { BasicExpression } from "../../../src/query/ir.js" - -// Import operators to register evaluators (needed for direct IR testing) -import "../../../src/query/builder/operators/index.js" +} from "../../../src/query/ir.js" describe(`custom operators`, () => { - describe(`registerOperator`, () => { - it(`allows registering a custom "between" operator`, () => { - // Register a custom "between" operator - const betweenFactory: EvaluatorFactory = ( - compiledArgs: Array, - _isSingleRow: boolean - ): CompiledExpression => { - const valueEval = compiledArgs[0]! - const minEval = compiledArgs[1]! - const maxEval = compiledArgs[2]! - - return (data: any) => { - const value = valueEval(data) - const min = minEval(data) - const max = maxEval(data) - - if (value === null || value === undefined) { - return null // 3-valued logic - } - - return value >= min && value <= max - } + // Define factory for the "between" operator + const betweenFactory: EvaluatorFactory = ( + compiledArgs: Array, + _isSingleRow: boolean + ): CompiledExpression => { + const valueEval = compiledArgs[0]! + const minEval = compiledArgs[1]! + const maxEval = compiledArgs[2]! + + return (data: any) => { + const value = valueEval(data) + const min = minEval(data) + const max = maxEval(data) + + if (value === null || value === undefined) { + return null // 3-valued logic } - registerOperator(`between`, betweenFactory) - + return value >= min && value <= max + } + } + + // Builder function for "between" operator + function between(value: any, min: any, max: any): BasicExpression { + return new Func( + `between`, + [toExpression(value), toExpression(min), toExpression(max)], + betweenFactory + ) + } + + describe(`custom operator pattern`, () => { + it(`allows creating a custom "between" operator`, () => { // Test the custom operator - const func = new Func(`between`, [ - new Value(5), - new Value(1), - new Value(10), - ]) + const func = between(new Value(5), new Value(1), new Value(10)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`custom "between" operator returns false when out of range`, () => { - const func = new Func(`between`, [ - new Value(15), - new Value(1), - new Value(10), - ]) + const func = between(new Value(15), new Value(1), new Value(10)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`custom "between" operator handles null with 3-valued logic`, () => { - const func = new Func(`between`, [ - new Value(null), - new Value(1), - new Value(10), - ]) + const func = between(new Value(null), new Value(1), new Value(10)) const compiled = compileExpression(func) expect(compiled({})).toBe(null) }) - it(`allows registering a custom "startsWith" operator`, () => { + it(`allows creating a custom "startsWith" operator`, () => { const startsWithFactory: EvaluatorFactory = ( compiledArgs: Array, _isSingleRow: boolean @@ -95,19 +87,22 @@ describe(`custom operators`, () => { } } - registerOperator(`startsWith`, startsWithFactory) + function startsWith(str: any, prefix: any): BasicExpression { + return new Func( + `startsWith`, + [toExpression(str), toExpression(prefix)], + startsWithFactory + ) + } - const func = new Func(`startsWith`, [ - new Value(`hello world`), - new Value(`hello`), - ]) + const func = startsWith(new Value(`hello world`), new Value(`hello`)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`custom operator works with property references`, () => { - // Register a custom "isEmpty" operator + // Define a custom "isEmpty" operator const isEmptyFactory: EvaluatorFactory = ( compiledArgs: Array, _isSingleRow: boolean @@ -131,10 +126,12 @@ describe(`custom operators`, () => { } } - registerOperator(`isEmpty`, isEmptyFactory) + function isEmpty(value: any): BasicExpression { + return new Func(`isEmpty`, [toExpression(value)], isEmptyFactory) + } // Test with a property reference - const func = new Func(`isEmpty`, [new PropRef([`users`, `name`])]) + const func = isEmpty(new PropRef([`users`, `name`])) const compiled = compileExpression(func) expect(compiled({ users: { name: `` } })).toBe(true) @@ -142,7 +139,7 @@ describe(`custom operators`, () => { expect(compiled({ users: { name: null } })).toBe(true) }) - it(`allows registering a custom "modulo" operator`, () => { + it(`allows creating a custom "modulo" operator`, () => { const moduloFactory: EvaluatorFactory = ( compiledArgs: Array, _isSingleRow: boolean @@ -165,27 +162,33 @@ describe(`custom operators`, () => { } } - registerOperator(`modulo`, moduloFactory) + function modulo(left: any, right: any): BasicExpression { + return new Func( + `modulo`, + [toExpression(left), toExpression(right)], + moduloFactory + ) + } - const func = new Func(`modulo`, [new Value(10), new Value(3)]) + const func = modulo(new Value(10), new Value(3)) const compiled = compileExpression(func) expect(compiled({})).toBe(1) }) it(`custom operator can be used in nested expressions`, () => { - // Use the previously registered "between" with an "and" operator - const func = new Func(`and`, [ - new Func(`between`, [new Value(5), new Value(1), new Value(10)]), - new Func(`between`, [new Value(15), new Value(10), new Value(20)]), - ]) + // Use the "between" operator with an "and" operator + const func = and( + between(new Value(5), new Value(1), new Value(10)), + between(new Value(15), new Value(10), new Value(20)) + ) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) - it(`custom operator can override built-in operator behavior`, () => { - // Register a custom version of "length" that handles objects + it(`custom operator with extended behavior`, () => { + // Define a custom version of "length" that also handles objects const customLengthFactory: EvaluatorFactory = ( compiledArgs: Array, _isSingleRow: boolean @@ -209,10 +212,15 @@ describe(`custom operators`, () => { } } - // This will override the built-in length operator - registerOperator(`customLength`, customLengthFactory) + function customLength(value: any): BasicExpression { + return new Func( + `customLength`, + [toExpression(value)], + customLengthFactory + ) + } - const func = new Func(`customLength`, [new Value({ a: 1, b: 2, c: 3 })]) + const func = customLength(new Value({ a: 1, b: 2, c: 3 })) const compiled = compileExpression(func) expect(compiled({})).toBe(3) @@ -220,29 +228,16 @@ describe(`custom operators`, () => { }) describe(`builder function pattern`, () => { - it(`can create a builder function for custom operators`, () => { + it(`demonstrates the full pattern for custom operators`, () => { // This demonstrates the full pattern users would use - // 1. Define the builder function (like eq, gt, etc.) - function between( - value: any, - min: any, - max: any - ): BasicExpression { - return new Func(`between`, [ - toExpression(value), - toExpression(min), - toExpression(max), - ]) - } - - // 2. The evaluator was already registered in previous tests - // In real usage, you'd register it alongside the builder + // 1. The builder function was already defined above (between) + // It includes both the factory and the builder function - // 3. Use it like any other operator + // 2. Use it like any other operator const expr = between(new PropRef([`users`, `age`]), 18, 65) - // 4. Compile and execute + // 3. Compile and execute const compiled = compileExpression(expr) expect(compiled({ users: { age: 30 } })).toBe(true) diff --git a/packages/db/tests/query/compiler/evaluators.test.ts b/packages/db/tests/query/compiler/evaluators.test.ts index 8551a8a2a..ac1e4c653 100644 --- a/packages/db/tests/query/compiler/evaluators.test.ts +++ b/packages/db/tests/query/compiler/evaluators.test.ts @@ -1,11 +1,30 @@ import { describe, expect, it } from "vitest" import { compileExpression } from "../../../src/query/compiler/evaluators.js" import { Func, PropRef, Value } from "../../../src/query/ir.js" +import { + add, + and, + coalesce, + concat, + divide, + eq, + gt, + gte, + ilike, + inArray, + length, + like, + lower, + lt, + lte, + multiply, + not, + or, + subtract, + upper, +} from "../../../src/query/builder/operators/index.js" import type { NamespacedRow } from "../../../src/types.js" -// Import operators to register evaluators (needed for direct IR testing) -import "../../../src/query/builder/operators/index.js" - describe(`evaluators`, () => { describe(`compileExpression`, () => { it(`handles unknown expression type`, () => { @@ -84,42 +103,42 @@ describe(`evaluators`, () => { describe(`string functions`, () => { it(`handles upper with non-string value`, () => { - const func = new Func(`upper`, [new Value(42)]) + const func = upper(new Value(42)) const compiled = compileExpression(func) expect(compiled({})).toBe(42) }) it(`handles lower with non-string value`, () => { - const func = new Func(`lower`, [new Value(true)]) + const func = lower(new Value(true)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles length with non-string, non-array value`, () => { - const func = new Func(`length`, [new Value(42)]) + const func = length(new Value(42)) const compiled = compileExpression(func) expect(compiled({})).toBe(0) }) it(`handles length with array`, () => { - const func = new Func(`length`, [new Value([1, 2, 3])]) + const func = length(new Value([1, 2, 3])) const compiled = compileExpression(func) expect(compiled({})).toBe(3) }) it(`handles concat with various types`, () => { - const func = new Func(`concat`, [ + const func = concat( new Value(`Hello`), new Value(null), new Value(undefined), new Value(42), new Value({ a: 1 }), - new Value([1, 2, 3]), - ]) + new Value([1, 2, 3]) + ) const compiled = compileExpression(func) const result = compiled({}) @@ -131,7 +150,7 @@ describe(`evaluators`, () => { const circular: any = {} circular.self = circular - const func = new Func(`concat`, [new Value(circular)]) + const func = concat(new Value(circular)) const compiled = compileExpression(func) // Should not throw and should return some fallback string @@ -140,22 +159,22 @@ describe(`evaluators`, () => { }) it(`handles coalesce with all null/undefined values`, () => { - const func = new Func(`coalesce`, [ + const func = coalesce( new Value(null), new Value(undefined), - new Value(null), - ]) + new Value(null) + ) const compiled = compileExpression(func) expect(compiled({})).toBeNull() }) it(`handles coalesce with first non-null value`, () => { - const func = new Func(`coalesce`, [ + const func = coalesce( new Value(null), new Value(`first`), - new Value(`second`), - ]) + new Value(`second`) + ) const compiled = compileExpression(func) expect(compiled({})).toBe(`first`) @@ -164,24 +183,21 @@ describe(`evaluators`, () => { describe(`array functions`, () => { it(`handles in with non-array value`, () => { - const func = new Func(`in`, [new Value(1), new Value(`not an array`)]) + const func = inArray(new Value(1), new Value(`not an array`)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`handles in with array`, () => { - const func = new Func(`in`, [ - new Value(2), - new Value([1, 2, 3, null]), - ]) + const func = inArray(new Value(2), new Value([1, 2, 3, null])) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles in with null value (3-valued logic)`, () => { - const func = new Func(`in`, [new Value(null), new Value([1, 2, 3])]) + const func = inArray(new Value(null), new Value([1, 2, 3])) const compiled = compileExpression(func) // In 3-valued logic, null in array returns UNKNOWN (null) @@ -189,10 +205,7 @@ describe(`evaluators`, () => { }) it(`handles in with undefined value (3-valued logic)`, () => { - const func = new Func(`in`, [ - new Value(undefined), - new Value([1, 2, 3]), - ]) + const func = inArray(new Value(undefined), new Value([1, 2, 3])) const compiled = compileExpression(func) // In 3-valued logic, undefined in array returns UNKNOWN (null) @@ -202,35 +215,35 @@ describe(`evaluators`, () => { describe(`math functions`, () => { it(`handles add with null values (should default to 0)`, () => { - const func = new Func(`add`, [new Value(null), new Value(undefined)]) + const func = add(new Value(null), new Value(undefined)) const compiled = compileExpression(func) expect(compiled({})).toBe(0) }) it(`handles subtract with null values`, () => { - const func = new Func(`subtract`, [new Value(null), new Value(5)]) + const func = subtract(new Value(null), new Value(5)) const compiled = compileExpression(func) expect(compiled({})).toBe(-5) }) it(`handles multiply with null values`, () => { - const func = new Func(`multiply`, [new Value(null), new Value(5)]) + const func = multiply(new Value(null), new Value(5)) const compiled = compileExpression(func) expect(compiled({})).toBe(0) }) it(`handles divide with zero divisor`, () => { - const func = new Func(`divide`, [new Value(10), new Value(0)]) + const func = divide(new Value(10), new Value(0)) const compiled = compileExpression(func) expect(compiled({})).toBeNull() }) it(`handles divide with null values`, () => { - const func = new Func(`divide`, [new Value(null), new Value(null)]) + const func = divide(new Value(null), new Value(null)) const compiled = compileExpression(func) expect(compiled({})).toBeNull() @@ -239,51 +252,42 @@ describe(`evaluators`, () => { describe(`like/ilike functions`, () => { it(`handles like with non-string value`, () => { - const func = new Func(`like`, [new Value(42), new Value(`%2%`)]) + const func = like(new Value(42), new Value(`%2%`)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`handles like with non-string pattern`, () => { - const func = new Func(`like`, [new Value(`hello`), new Value(42)]) + const func = like(new Value(`hello`), new Value(42)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`handles like with wildcard patterns`, () => { - const func = new Func(`like`, [ - new Value(`hello world`), - new Value(`hello%`), - ]) + const func = like(new Value(`hello world`), new Value(`hello%`)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles like with single character wildcard`, () => { - const func = new Func(`like`, [ - new Value(`hello`), - new Value(`hell_`), - ]) + const func = like(new Value(`hello`), new Value(`hell_`)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles like with regex special characters`, () => { - const func = new Func(`like`, [ - new Value(`test.string`), - new Value(`test.string`), - ]) + const func = like(new Value(`test.string`), new Value(`test.string`)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles like with null value (3-valued logic)`, () => { - const func = new Func(`like`, [new Value(null), new Value(`hello%`)]) + const func = like(new Value(null), new Value(`hello%`)) const compiled = compileExpression(func) // In 3-valued logic, like with null value returns UNKNOWN (null) @@ -291,10 +295,7 @@ describe(`evaluators`, () => { }) it(`handles like with undefined value (3-valued logic)`, () => { - const func = new Func(`like`, [ - new Value(undefined), - new Value(`hello%`), - ]) + const func = like(new Value(undefined), new Value(`hello%`)) const compiled = compileExpression(func) // In 3-valued logic, like with undefined value returns UNKNOWN (null) @@ -302,7 +303,7 @@ describe(`evaluators`, () => { }) it(`handles like with null pattern (3-valued logic)`, () => { - const func = new Func(`like`, [new Value(`hello`), new Value(null)]) + const func = like(new Value(`hello`), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, like with null pattern returns UNKNOWN (null) @@ -310,10 +311,7 @@ describe(`evaluators`, () => { }) it(`handles like with undefined pattern (3-valued logic)`, () => { - const func = new Func(`like`, [ - new Value(`hello`), - new Value(undefined), - ]) + const func = like(new Value(`hello`), new Value(undefined)) const compiled = compileExpression(func) // In 3-valued logic, like with undefined pattern returns UNKNOWN (null) @@ -321,27 +319,21 @@ describe(`evaluators`, () => { }) it(`handles ilike (case insensitive)`, () => { - const func = new Func(`ilike`, [ - new Value(`HELLO`), - new Value(`hello`), - ]) + const func = ilike(new Value(`HELLO`), new Value(`hello`)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles ilike with patterns`, () => { - const func = new Func(`ilike`, [ - new Value(`HELLO WORLD`), - new Value(`hello%`), - ]) + const func = ilike(new Value(`HELLO WORLD`), new Value(`hello%`)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles ilike with null value (3-valued logic)`, () => { - const func = new Func(`ilike`, [new Value(null), new Value(`hello%`)]) + const func = ilike(new Value(null), new Value(`hello%`)) const compiled = compileExpression(func) // In 3-valued logic, ilike with null value returns UNKNOWN (null) @@ -349,10 +341,7 @@ describe(`evaluators`, () => { }) it(`handles ilike with undefined value (3-valued logic)`, () => { - const func = new Func(`ilike`, [ - new Value(undefined), - new Value(`hello%`), - ]) + const func = ilike(new Value(undefined), new Value(`hello%`)) const compiled = compileExpression(func) // In 3-valued logic, ilike with undefined value returns UNKNOWN (null) @@ -360,7 +349,7 @@ describe(`evaluators`, () => { }) it(`handles ilike with null pattern (3-valued logic)`, () => { - const func = new Func(`ilike`, [new Value(`hello`), new Value(null)]) + const func = ilike(new Value(`hello`), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, ilike with null pattern returns UNKNOWN (null) @@ -368,10 +357,7 @@ describe(`evaluators`, () => { }) it(`handles ilike with undefined pattern (3-valued logic)`, () => { - const func = new Func(`ilike`, [ - new Value(`hello`), - new Value(undefined), - ]) + const func = ilike(new Value(`hello`), new Value(undefined)) const compiled = compileExpression(func) // In 3-valued logic, ilike with undefined pattern returns UNKNOWN (null) @@ -382,7 +368,7 @@ describe(`evaluators`, () => { describe(`comparison operators`, () => { describe(`eq (equality)`, () => { it(`handles eq with null and null (3-valued logic)`, () => { - const func = new Func(`eq`, [new Value(null), new Value(null)]) + const func = eq(new Value(null), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, null = null returns UNKNOWN (null) @@ -390,7 +376,7 @@ describe(`evaluators`, () => { }) it(`handles eq with null and value (3-valued logic)`, () => { - const func = new Func(`eq`, [new Value(null), new Value(5)]) + const func = eq(new Value(null), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, null = value returns UNKNOWN (null) @@ -398,7 +384,7 @@ describe(`evaluators`, () => { }) it(`handles eq with value and null (3-valued logic)`, () => { - const func = new Func(`eq`, [new Value(5), new Value(null)]) + const func = eq(new Value(5), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, value = null returns UNKNOWN (null) @@ -406,7 +392,7 @@ describe(`evaluators`, () => { }) it(`handles eq with undefined and value (3-valued logic)`, () => { - const func = new Func(`eq`, [new Value(undefined), new Value(5)]) + const func = eq(new Value(undefined), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, undefined = value returns UNKNOWN (null) @@ -414,14 +400,14 @@ describe(`evaluators`, () => { }) it(`handles eq with matching values`, () => { - const func = new Func(`eq`, [new Value(5), new Value(5)]) + const func = eq(new Value(5), new Value(5)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles eq with non-matching values`, () => { - const func = new Func(`eq`, [new Value(5), new Value(10)]) + const func = eq(new Value(5), new Value(10)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) @@ -430,7 +416,7 @@ describe(`evaluators`, () => { it(`handles eq with matching Uint8Arrays (content equality)`, () => { const array1 = new Uint8Array([1, 2, 3, 4, 5]) const array2 = new Uint8Array([1, 2, 3, 4, 5]) - const func = new Func(`eq`, [new Value(array1), new Value(array2)]) + const func = eq(new Value(array1), new Value(array2)) const compiled = compileExpression(func) // Should return true because content is the same @@ -440,7 +426,7 @@ describe(`evaluators`, () => { it(`handles eq with non-matching Uint8Arrays (different content)`, () => { const array1 = new Uint8Array([1, 2, 3, 4, 5]) const array2 = new Uint8Array([1, 2, 3, 4, 6]) - const func = new Func(`eq`, [new Value(array1), new Value(array2)]) + const func = eq(new Value(array1), new Value(array2)) const compiled = compileExpression(func) // Should return false because content is different @@ -450,7 +436,7 @@ describe(`evaluators`, () => { it(`handles eq with Uint8Arrays of different lengths`, () => { const array1 = new Uint8Array([1, 2, 3, 4]) const array2 = new Uint8Array([1, 2, 3, 4, 5]) - const func = new Func(`eq`, [new Value(array1), new Value(array2)]) + const func = eq(new Value(array1), new Value(array2)) const compiled = compileExpression(func) // Should return false because lengths are different @@ -459,7 +445,7 @@ describe(`evaluators`, () => { it(`handles eq with same Uint8Array reference`, () => { const array = new Uint8Array([1, 2, 3, 4, 5]) - const func = new Func(`eq`, [new Value(array), new Value(array)]) + const func = eq(new Value(array), new Value(array)) const compiled = compileExpression(func) // Should return true (fast path for reference equality) @@ -469,7 +455,8 @@ describe(`evaluators`, () => { it(`handles eq with Uint8Array and non-Uint8Array`, () => { const array = new Uint8Array([1, 2, 3]) const value = [1, 2, 3] - const func = new Func(`eq`, [new Value(array), new Value(value)]) + // Cast to any to test runtime behavior with mismatched types + const func = eq(new Value(array) as any, new Value(value) as any) const compiled = compileExpression(func) // Should return false because types are different @@ -487,7 +474,7 @@ describe(`evaluators`, () => { ulid2[i] = i } - const func = new Func(`eq`, [new Value(ulid1), new Value(ulid2)]) + const func = eq(new Value(ulid1), new Value(ulid2)) const compiled = compileExpression(func) // Should return true because content is identical @@ -498,10 +485,7 @@ describe(`evaluators`, () => { if (typeof Buffer !== `undefined`) { const buffer1 = Buffer.from([1, 2, 3, 4, 5]) const buffer2 = Buffer.from([1, 2, 3, 4, 5]) - const func = new Func(`eq`, [ - new Value(buffer1), - new Value(buffer2), - ]) + const func = eq(new Value(buffer1), new Value(buffer2)) const compiled = compileExpression(func) // Should return true because content is the same @@ -513,7 +497,7 @@ describe(`evaluators`, () => { // Reproduction of user's issue: new Uint8Array(5) creates [0,0,0,0,0] const array1 = new Uint8Array(5) // Creates array of length 5, all zeros const array2 = new Uint8Array(5) // Creates another array of length 5, all zeros - const func = new Func(`eq`, [new Value(array1), new Value(array2)]) + const func = eq(new Value(array1), new Value(array2)) const compiled = compileExpression(func) // Should return true because both have same content (all zeros) @@ -523,7 +507,7 @@ describe(`evaluators`, () => { it(`handles eq with empty Uint8Arrays`, () => { const array1 = new Uint8Array(0) const array2 = new Uint8Array(0) - const func = new Func(`eq`, [new Value(array1), new Value(array2)]) + const func = eq(new Value(array1), new Value(array2)) const compiled = compileExpression(func) // Empty arrays should be equal @@ -531,27 +515,21 @@ describe(`evaluators`, () => { }) it(`still handles eq with strings correctly`, () => { - const func1 = new Func(`eq`, [ - new Value(`hello`), - new Value(`hello`), - ]) + const func1 = eq(new Value(`hello`), new Value(`hello`)) const compiled1 = compileExpression(func1) expect(compiled1({})).toBe(true) - const func2 = new Func(`eq`, [ - new Value(`hello`), - new Value(`world`), - ]) + const func2 = eq(new Value(`hello`), new Value(`world`)) const compiled2 = compileExpression(func2) expect(compiled2({})).toBe(false) }) it(`still handles eq with numbers correctly`, () => { - const func1 = new Func(`eq`, [new Value(42), new Value(42)]) + const func1 = eq(new Value(42), new Value(42)) const compiled1 = compileExpression(func1) expect(compiled1({})).toBe(true) - const func2 = new Func(`eq`, [new Value(42), new Value(43)]) + const func2 = eq(new Value(42), new Value(43)) const compiled2 = compileExpression(func2) expect(compiled2({})).toBe(false) }) @@ -559,7 +537,7 @@ describe(`evaluators`, () => { describe(`gt (greater than)`, () => { it(`handles gt with null and value (3-valued logic)`, () => { - const func = new Func(`gt`, [new Value(null), new Value(5)]) + const func = gt(new Value(null), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, null > value returns UNKNOWN (null) @@ -567,7 +545,7 @@ describe(`evaluators`, () => { }) it(`handles gt with value and null (3-valued logic)`, () => { - const func = new Func(`gt`, [new Value(5), new Value(null)]) + const func = gt(new Value(5), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, value > null returns UNKNOWN (null) @@ -575,7 +553,7 @@ describe(`evaluators`, () => { }) it(`handles gt with undefined (3-valued logic)`, () => { - const func = new Func(`gt`, [new Value(undefined), new Value(5)]) + const func = gt(new Value(undefined), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, undefined > value returns UNKNOWN (null) @@ -583,7 +561,7 @@ describe(`evaluators`, () => { }) it(`handles gt with valid values`, () => { - const func = new Func(`gt`, [new Value(10), new Value(5)]) + const func = gt(new Value(10), new Value(5)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) @@ -592,7 +570,7 @@ describe(`evaluators`, () => { describe(`gte (greater than or equal)`, () => { it(`handles gte with null (3-valued logic)`, () => { - const func = new Func(`gte`, [new Value(null), new Value(5)]) + const func = gte(new Value(null), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, null >= value returns UNKNOWN (null) @@ -600,7 +578,7 @@ describe(`evaluators`, () => { }) it(`handles gte with undefined (3-valued logic)`, () => { - const func = new Func(`gte`, [new Value(undefined), new Value(5)]) + const func = gte(new Value(undefined), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, undefined >= value returns UNKNOWN (null) @@ -610,7 +588,7 @@ describe(`evaluators`, () => { describe(`lt (less than)`, () => { it(`handles lt with null (3-valued logic)`, () => { - const func = new Func(`lt`, [new Value(null), new Value(5)]) + const func = lt(new Value(null), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, null < value returns UNKNOWN (null) @@ -618,7 +596,7 @@ describe(`evaluators`, () => { }) it(`handles lt with undefined (3-valued logic)`, () => { - const func = new Func(`lt`, [new Value(undefined), new Value(5)]) + const func = lt(new Value(undefined), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, undefined < value returns UNKNOWN (null) @@ -626,7 +604,7 @@ describe(`evaluators`, () => { }) it(`handles lt with valid values`, () => { - const func = new Func(`lt`, [new Value(3), new Value(5)]) + const func = lt(new Value(3), new Value(5)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) @@ -635,7 +613,7 @@ describe(`evaluators`, () => { describe(`lte (less than or equal)`, () => { it(`handles lte with null (3-valued logic)`, () => { - const func = new Func(`lte`, [new Value(null), new Value(5)]) + const func = lte(new Value(null), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, null <= value returns UNKNOWN (null) @@ -643,7 +621,7 @@ describe(`evaluators`, () => { }) it(`handles lte with undefined (3-valued logic)`, () => { - const func = new Func(`lte`, [new Value(undefined), new Value(5)]) + const func = lte(new Value(undefined), new Value(5)) const compiled = compileExpression(func) // In 3-valued logic, undefined <= value returns UNKNOWN (null) @@ -654,17 +632,14 @@ describe(`evaluators`, () => { describe(`boolean operators`, () => { it(`handles and with short-circuit evaluation`, () => { - const func = new Func(`and`, [ - new Value(false), - new Func(`divide`, [new Value(1), new Value(0)]), // This would return null, but shouldn't be evaluated - ]) + const func = and(new Value(false), divide(new Value(1), new Value(0))) // This would return null, but shouldn't be evaluated const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`handles and with null value (3-valued logic)`, () => { - const func = new Func(`and`, [new Value(true), new Value(null)]) + const func = and(new Value(true), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, true AND null = null (UNKNOWN) @@ -672,7 +647,7 @@ describe(`evaluators`, () => { }) it(`handles and with undefined value (3-valued logic)`, () => { - const func = new Func(`and`, [new Value(true), new Value(undefined)]) + const func = and(new Value(true), new Value(undefined)) const compiled = compileExpression(func) // In 3-valued logic, true AND undefined = null (UNKNOWN) @@ -680,7 +655,7 @@ describe(`evaluators`, () => { }) it(`handles and with null and false (3-valued logic)`, () => { - const func = new Func(`and`, [new Value(null), new Value(false)]) + const func = and(new Value(null), new Value(false)) const compiled = compileExpression(func) // In 3-valued logic, null AND false = false @@ -688,28 +663,21 @@ describe(`evaluators`, () => { }) it(`handles and with all true values`, () => { - const func = new Func(`and`, [new Value(true), new Value(true)]) + const func = and(new Value(true), new Value(true)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles or with short-circuit evaluation`, () => { - const func = new Func(`or`, [ - new Value(true), - new Func(`divide`, [new Value(1), new Value(0)]), // This would return null, but shouldn't be evaluated - ]) + const func = or(new Value(true), divide(new Value(1), new Value(0))) // This would return null, but shouldn't be evaluated const compiled = compileExpression(func) expect(compiled({})).toBe(true) }) it(`handles or with null value (3-valued logic)`, () => { - const func = new Func(`or`, [ - new Value(false), - new Value(0), - new Value(null), - ]) + const func = or(new Value(false), new Value(0), new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, false OR null = null (UNKNOWN) @@ -717,18 +685,14 @@ describe(`evaluators`, () => { }) it(`handles or with undefined value (3-valued logic)`, () => { - const func = new Func(`or`, [ - new Value(false), - new Value(0), - new Value(undefined), - ]) + const func = or(new Value(false), new Value(0), new Value(undefined)) const compiled = compileExpression(func) // In 3-valued logic, false OR undefined = null (UNKNOWN) expect(compiled({})).toBe(null) }) it(`handles or with null and true (3-valued logic)`, () => { - const func = new Func(`or`, [new Value(null), new Value(true)]) + const func = or(new Value(null), new Value(true)) const compiled = compileExpression(func) // In 3-valued logic, null OR true = true @@ -736,14 +700,14 @@ describe(`evaluators`, () => { }) it(`handles or with all false values`, () => { - const func = new Func(`or`, [new Value(false), new Value(0)]) + const func = or(new Value(false), new Value(0)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`handles not with null value (3-valued logic)`, () => { - const func = new Func(`not`, [new Value(null)]) + const func = not(new Value(null)) const compiled = compileExpression(func) // In 3-valued logic, NOT null = null (UNKNOWN) @@ -751,7 +715,7 @@ describe(`evaluators`, () => { }) it(`handles not with undefined value (3-valued logic)`, () => { - const func = new Func(`not`, [new Value(undefined)]) + const func = not(new Value(undefined)) const compiled = compileExpression(func) // In 3-valued logic, NOT undefined = null (UNKNOWN) @@ -759,14 +723,14 @@ describe(`evaluators`, () => { }) it(`handles not with true value`, () => { - const func = new Func(`not`, [new Value(true)]) + const func = not(new Value(true)) const compiled = compileExpression(func) expect(compiled({})).toBe(false) }) it(`handles not with false value`, () => { - const func = new Func(`not`, [new Value(false)]) + const func = not(new Value(false)) const compiled = compileExpression(func) expect(compiled({})).toBe(true) diff --git a/packages/db/tests/query/compiler/select.test.ts b/packages/db/tests/query/compiler/select.test.ts index d2825bf95..a744d76dc 100644 --- a/packages/db/tests/query/compiler/select.test.ts +++ b/packages/db/tests/query/compiler/select.test.ts @@ -1,9 +1,14 @@ import { describe, expect, it } from "vitest" import { processArgument } from "../../../src/query/compiler/select.js" -import { Aggregate, Func, PropRef, Value } from "../../../src/query/ir.js" - -// Import operators to register evaluators (needed for direct IR testing) -import "../../../src/query/builder/operators/index.js" +import { Aggregate, PropRef, Value } from "../../../src/query/ir.js" +import { + add, + and, + concat, + gt, + length, + upper, +} from "../../../src/query/builder/operators/index.js" describe(`select compiler`, () => { // Note: Most of the select compilation logic is tested through the full integration @@ -28,7 +33,7 @@ describe(`select compiler`, () => { }) it(`processes function expressions correctly`, () => { - const arg = new Func(`upper`, [new Value(`hello`)]) + const arg = upper(new Value(`hello`)) const namespacedRow = {} const result = processArgument(arg, namespacedRow) @@ -72,7 +77,7 @@ describe(`select compiler`, () => { }) it(`processes function expressions with references`, () => { - const arg = new Func(`length`, [new PropRef([`users`, `name`])]) + const arg = length(new PropRef([`users`, `name`])) const namespacedRow = { users: { name: `Alice` } } const result = processArgument(arg, namespacedRow) @@ -80,11 +85,11 @@ describe(`select compiler`, () => { }) it(`processes function expressions with multiple arguments`, () => { - const arg = new Func(`concat`, [ + const arg = concat( new PropRef([`users`, `firstName`]), new Value(` `), - new PropRef([`users`, `lastName`]), - ]) + new PropRef([`users`, `lastName`]) + ) const namespacedRow = { users: { firstName: `John`, @@ -129,7 +134,7 @@ describe(`select compiler`, () => { }) it(`processes boolean function expressions`, () => { - const arg = new Func(`and`, [new Value(true), new Value(false)]) + const arg = and(new Value(true), new Value(false)) const namespacedRow = {} const result = processArgument(arg, namespacedRow) @@ -137,7 +142,7 @@ describe(`select compiler`, () => { }) it(`processes comparison function expressions`, () => { - const arg = new Func(`gt`, [new PropRef([`users`, `age`]), new Value(18)]) + const arg = gt(new PropRef([`users`, `age`]), new Value(18)) const namespacedRow = { users: { age: 25 } } const result = processArgument(arg, namespacedRow) @@ -145,10 +150,10 @@ describe(`select compiler`, () => { }) it(`processes mathematical function expressions`, () => { - const arg = new Func(`add`, [ + const arg = add( new PropRef([`order`, `subtotal`]), - new PropRef([`order`, `tax`]), - ]) + new PropRef([`order`, `tax`]) + ) const namespacedRow = { order: { subtotal: 100, @@ -195,8 +200,8 @@ describe(`select compiler`, () => { const nonAggregateExpressions = [ new PropRef([`users`, `name`]), new Value(42), - new Func(`upper`, [new Value(`hello`)]), - new Func(`length`, [new PropRef([`users`, `name`])]), + upper(new Value(`hello`)), + length(new PropRef([`users`, `name`])), ] const namespacedRow = { users: { name: `John` } }