UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

387 lines (358 loc) 10 kB
import { EmptyReferencePathError, UnknownExpressionTypeError, UnknownFunctionError, } from "../../errors.js" import type { BasicExpression, Func, PropRef } from "../ir.js" import type { NamespacedRow } from "../../types.js" /** * Compiled expression evaluator function type */ export type CompiledExpression = (namespacedRow: NamespacedRow) => any /** * Compiled single-row expression evaluator function type */ export type CompiledSingleRowExpression = (item: Record<string, unknown>) => any /** * Compiles an expression into an optimized evaluator function. * This eliminates branching during evaluation by pre-compiling the expression structure. */ export function compileExpression( expr: BasicExpression, isSingleRow: boolean = false ): CompiledExpression | CompiledSingleRowExpression { const compiledFn = compileExpressionInternal(expr, isSingleRow) return compiledFn } /** * Compiles a single-row expression into an optimized evaluator function. */ export function compileSingleRowExpression( expr: BasicExpression ): CompiledSingleRowExpression { const compiledFn = compileExpressionInternal(expr, true) return compiledFn as CompiledSingleRowExpression } /** * Internal unified expression compiler that handles both namespaced and single-row evaluation */ function compileExpressionInternal( expr: BasicExpression, isSingleRow: boolean ): (data: any) => any { switch (expr.type) { case `val`: { // For constant values, return a function that just returns the value const value = expr.value return () => value } case `ref`: { // For references, compile based on evaluation mode return isSingleRow ? compileSingleRowRef(expr) : compileRef(expr) } case `func`: { // For functions, use the unified compiler return compileFunction(expr, isSingleRow) } default: throw new UnknownExpressionTypeError((expr as any).type) } } /** * Compiles a reference expression into an optimized evaluator */ function compileRef(ref: PropRef): CompiledExpression { const [tableAlias, ...propertyPath] = ref.path if (!tableAlias) { throw new EmptyReferencePathError() } // Pre-compile the property path navigation if (propertyPath.length === 0) { // Simple table reference return (namespacedRow) => namespacedRow[tableAlias] } else if (propertyPath.length === 1) { // Single property access - most common case const prop = propertyPath[0]! return (namespacedRow) => { const tableData = namespacedRow[tableAlias] return tableData?.[prop] } } else { // Multiple property navigation return (namespacedRow) => { const tableData = namespacedRow[tableAlias] if (tableData === undefined) { return undefined } let value: any = tableData for (const prop of propertyPath) { if (value == null) { return value } value = value[prop] } return value } } } /** * Compiles a reference expression for single-row evaluation */ function compileSingleRowRef(ref: PropRef): CompiledSingleRowExpression { const propertyPath = ref.path // This function works for all path lengths including empty path return (item) => { let value: any = item for (const prop of propertyPath) { if (value == null) { return value } value = value[prop] } return value } } /** * Compiles a function expression for both namespaced and single-row evaluation */ function compileFunction(func: Func, isSingleRow: boolean): (data: any) => any { // Pre-compile all arguments using the appropriate compiler const compiledArgs = func.args.map((arg) => compileExpressionInternal(arg, isSingleRow) ) switch (func.name) { // Comparison operators case `eq`: { const argA = compiledArgs[0]! const argB = compiledArgs[1]! return (data) => { const a = argA(data) const b = argB(data) return a === b } } case `gt`: { const argA = compiledArgs[0]! const argB = compiledArgs[1]! return (data) => { const a = argA(data) const b = argB(data) return a > b } } case `gte`: { const argA = compiledArgs[0]! const argB = compiledArgs[1]! return (data) => { const a = argA(data) const b = argB(data) return a >= b } } case `lt`: { const argA = compiledArgs[0]! const argB = compiledArgs[1]! return (data) => { const a = argA(data) const b = argB(data) return a < b } } case `lte`: { const argA = compiledArgs[0]! const argB = compiledArgs[1]! return (data) => { const a = argA(data) const b = argB(data) return a <= b } } // Boolean operators case `and`: return (data) => { for (const compiledArg of compiledArgs) { if (!compiledArg(data)) { return false } } return true } case `or`: return (data) => { for (const compiledArg of compiledArgs) { if (compiledArg(data)) { return true } } return false } case `not`: { const arg = compiledArgs[0]! return (data) => !arg(data) } // Array operators case `in`: { const valueEvaluator = compiledArgs[0]! const arrayEvaluator = compiledArgs[1]! return (data) => { const value = valueEvaluator(data) const array = arrayEvaluator(data) 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) 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) 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 } } 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) }