@tanstack/db
Version:
A reactive client store for building super fast apps on sync
266 lines (247 loc) • 7.48 kB
text/typescript
import { map } from '@tanstack/db-ivm'
import { PropRef, Value as ValClass, isExpressionLike } from '../ir.js'
import { AggregateNotSupportedError } from '../../errors.js'
import { compileExpression } from './evaluators.js'
import type { Aggregate, BasicExpression, Select } from '../ir.js'
import type {
KeyedStream,
NamespacedAndKeyedStream,
NamespacedRow,
} from '../../types.js'
/**
* Type for operations array used in select processing
*/
type SelectOp =
| {
kind: `merge`
targetPath: Array<string>
source: (row: NamespacedRow) => any
}
| { kind: `field`; alias: string; compiled: (row: NamespacedRow) => any }
/**
* Unwraps any Value expressions
*/
function unwrapVal(input: any): any {
if (input instanceof ValClass) return input.value
return input
}
/**
* Processes a merge operation by merging source values into the target path
*/
function processMerge(
op: Extract<SelectOp, { kind: `merge` }>,
namespacedRow: NamespacedRow,
selectResults: Record<string, any>,
): void {
const value = op.source(namespacedRow)
if (value && typeof value === `object`) {
// Ensure target object exists
let cursor: any = selectResults
const path = op.targetPath
if (path.length === 0) {
// Top-level merge
for (const [k, v] of Object.entries(value)) {
selectResults[k] = unwrapVal(v)
}
} else {
for (let i = 0; i < path.length; i++) {
const seg = path[i]!
if (i === path.length - 1) {
const dest = (cursor[seg] ??= {})
if (typeof dest === `object`) {
for (const [k, v] of Object.entries(value)) {
dest[k] = unwrapVal(v)
}
}
} else {
const next = cursor[seg]
if (next == null || typeof next !== `object`) {
cursor[seg] = {}
}
cursor = cursor[seg]
}
}
}
}
}
/**
* Processes a non-merge operation by setting the field value at the specified alias path
*/
function processNonMergeOp(
op: Extract<SelectOp, { kind: `field` }>,
namespacedRow: NamespacedRow,
selectResults: Record<string, any>,
): void {
// Support nested alias paths like "meta.author.name"
const path = op.alias.split(`.`)
if (path.length === 1) {
selectResults[op.alias] = op.compiled(namespacedRow)
} else {
let cursor: any = selectResults
for (let i = 0; i < path.length - 1; i++) {
const seg = path[i]!
const next = cursor[seg]
if (next == null || typeof next !== `object`) {
cursor[seg] = {}
}
cursor = cursor[seg]
}
cursor[path[path.length - 1]!] = unwrapVal(op.compiled(namespacedRow))
}
}
/**
* Processes a single row to generate select results
*/
function processRow(
[key, namespacedRow]: [unknown, NamespacedRow],
ops: Array<SelectOp>,
): [unknown, typeof namespacedRow & { __select_results: any }] {
const selectResults: Record<string, any> = {}
for (const op of ops) {
if (op.kind === `merge`) {
processMerge(op, namespacedRow, selectResults)
} else {
processNonMergeOp(op, namespacedRow, selectResults)
}
}
// Return the namespaced row with __select_results added
return [
key,
{
...namespacedRow,
__select_results: selectResults,
},
] as [
unknown,
typeof namespacedRow & { __select_results: typeof selectResults },
]
}
/**
* Processes the SELECT clause and places results in __select_results
* while preserving the original namespaced row for ORDER BY access
*/
export function processSelect(
pipeline: NamespacedAndKeyedStream,
select: Select,
_allInputs: Record<string, KeyedStream>,
): NamespacedAndKeyedStream {
// Build ordered operations to preserve authoring order (spreads and fields)
const ops: Array<SelectOp> = []
addFromObject([], select, ops)
return pipeline.pipe(map((row) => processRow(row, ops)))
}
/**
* Helper function to check if an expression is an aggregate
*/
function isAggregateExpression(
expr: BasicExpression | Aggregate,
): expr is Aggregate {
return expr.type === `agg`
}
/**
* Processes a single argument in a function context
*/
export function processArgument(
arg: BasicExpression | Aggregate,
namespacedRow: NamespacedRow,
): any {
if (isAggregateExpression(arg)) {
throw new AggregateNotSupportedError()
}
// Pre-compile the expression and evaluate immediately
const compiledExpression = compileExpression(arg)
const value = compiledExpression(namespacedRow)
return value
}
/**
* Helper function to check if an object is a nested select object
*
* .select({
* id: users.id,
* profile: { // <-- this is a nested select object
* name: users.name,
* email: users.email
* }
* })
*/
function isNestedSelectObject(obj: any): boolean {
return obj && typeof obj === `object` && !isExpressionLike(obj)
}
/**
* Helper function to process select objects and build operations array
*/
function addFromObject(
prefixPath: Array<string>,
obj: any,
ops: Array<SelectOp>,
) {
for (const [key, value] of Object.entries(obj)) {
if (key.startsWith(`__SPREAD_SENTINEL__`)) {
const rest = key.slice(`__SPREAD_SENTINEL__`.length)
const splitIndex = rest.lastIndexOf(`__`)
const pathStr = splitIndex >= 0 ? rest.slice(0, splitIndex) : rest
const isRefExpr =
value &&
typeof value === `object` &&
`type` in (value as any) &&
(value as any).type === `ref`
if (pathStr.includes(`.`) || isRefExpr) {
// Merge into the current destination (prefixPath) from the referenced source path
const targetPath = [...prefixPath]
const expr = isRefExpr
? (value as BasicExpression)
: (new PropRef(pathStr.split(`.`)) as BasicExpression)
const compiled = compileExpression(expr)
ops.push({ kind: `merge`, targetPath, source: compiled })
} else {
// Table-level: pathStr is the alias; merge from namespaced row at the current prefix
const tableAlias = pathStr
const targetPath = [...prefixPath]
ops.push({
kind: `merge`,
targetPath,
source: (row) => (row as any)[tableAlias],
})
}
continue
}
const expression = value as any
if (isNestedSelectObject(expression)) {
// Nested selection object
addFromObject([...prefixPath, key], expression, ops)
continue
}
if (isAggregateExpression(expression)) {
// Placeholder for group-by processing later
ops.push({
kind: `field`,
alias: [...prefixPath, key].join(`.`),
compiled: () => null,
})
} else {
if (expression === undefined || !isExpressionLike(expression)) {
ops.push({
kind: `field`,
alias: [...prefixPath, key].join(`.`),
compiled: () => expression,
})
continue
}
// If the expression is a Value wrapper, embed the literal to avoid re-compilation mishaps
if (expression instanceof ValClass) {
const val = expression.value
ops.push({
kind: `field`,
alias: [...prefixPath, key].join(`.`),
compiled: () => val,
})
} else {
ops.push({
kind: `field`,
alias: [...prefixPath, key].join(`.`),
compiled: compileExpression(expression as BasicExpression),
})
}
}
}
}