@tanstack/db
Version:
A reactive client store for building super fast apps on sync
775 lines (700 loc) • 23.7 kB
text/typescript
import { Aggregate, Func, isBasicOrAggregateExpression } from '../ir'
import { codedMessage, devBuild } from '../../error-message.js'
import { grouping } from '../compiler/grouping.js'
import { isRefProxy, toExpression } from './ref-proxy.js'
import { registerWrapper } from './wrapper-identity.js'
import type { BasicExpression } from '../ir'
import type { IncludesRuntime } from '../live/includes-runtime.js'
import type { RefProxy } from './ref-proxy.js'
import type {
ContainsAggregate,
Context,
RefLeaf,
StringifiableScalar,
} from './types.js'
import type { BaseQueryBuilder, QueryBuilder } from './index.js'
// Aggregate provenance is a type-only fact. Wrapping an expression must not
// hide the implicit group from a downstream QueryRef's result type.
type WithAggregate<TResult, TInputs> =
true extends ContainsAggregate<TInputs>
? TResult & { readonly __hasAggregate: true }
: TResult
type StringRef =
RefLeaf<string> | RefLeaf<string | null> | RefLeaf<string | undefined>
type StringRefProxy =
RefProxy<string> | RefProxy<string | null> | RefProxy<string | undefined>
type StringBasicExpression =
| BasicExpression<string>
| BasicExpression<string | null>
| BasicExpression<string | undefined>
type StringLike =
StringRef | StringRefProxy | StringBasicExpression | string | null | undefined
type ComparisonOperand<T> =
RefProxy<T> | RefLeaf<T> | T | BasicExpression<T> | undefined | null
type ComparisonOperandPrimitive<T extends string | number | boolean> =
T | BasicExpression<T> | undefined | null
// Helper type for values that can be lowered to expressions.
type ExpressionLike =
| Aggregate
| BasicExpression
| RefProxy<any>
| RefLeaf<any>
| string
| number
| boolean
| bigint
| Date
| null
| undefined
| Array<unknown>
export type CaseWhenValue =
| ExpressionLike
| IncludeWrapper<any>
| ToArrayWrapper<any>
| ConcatToArrayWrapper<any>
// A select object. A query builder is not one: wrap it in include().
| (Record<string, any> & { _bindCollectionSources?: never })
type ExtractCaseWhenValue<T> =
T extends CaseWhenWrapper<infer TResult> ? TResult : T
type CaseWhenResult<
TValues extends Array<CaseWhenValue>,
THasDefault extends boolean,
TConditions = never,
> = WithAggregate<
TValues[number] extends ExpressionLike
? BasicExpression<
ExtractType<TValues[number]> | (THasDefault extends true ? never : null)
>
: CaseWhenWrapper<
| ExtractCaseWhenValue<TValues[number]>
| (THasDefault extends true ? never : undefined)
>,
[TValues, TConditions]
>
// Keep the first six conditions expression-like, as the former overloads did.
// Beyond five pairs the public result retains its existing any fallback.
type CaseWhenInputs<
T extends Array<CaseWhenValue>,
TPairs extends Array<unknown> = [],
> = TPairs[`length`] extends 6
? T
: T extends [unknown, unknown, ...infer Rest extends Array<CaseWhenValue>]
? [ExpressionLike, CaseWhenValue, ...CaseWhenInputs<Rest, [...TPairs, 0]>]
: T
type CaseWhenOutput<
T extends Array<CaseWhenValue>,
TValues extends Array<CaseWhenValue> = [],
TConditions extends Array<ExpressionLike> = [],
> = T extends [infer Default extends CaseWhenValue]
? CaseWhenResult<[...TValues, Default], true, TConditions>
: T extends []
? CaseWhenResult<TValues, false, TConditions>
: TConditions[`length`] extends 5
? any
: T extends [
infer Condition extends ExpressionLike,
infer Value extends CaseWhenValue,
...infer Rest extends Array<CaseWhenValue>,
]
? CaseWhenOutput<Rest, [...TValues, Value], [...TConditions, Condition]>
: never
// Helper type to extract the underlying type from various expression types
type ExtractType<T> =
T extends RefProxy<infer U>
? U
: T extends RefLeaf<infer U>
? U
: T extends BasicExpression<infer U> | Aggregate<infer U>
? U
: T
type IsAny<T> = 0 extends 1 & T ? true : false
type AggregateArgument<T, Domain> = T &
(IsAny<ExtractType<T>> extends true
? unknown
: [Exclude<ExtractType<T>, null | undefined>] extends [never]
? never
: [Exclude<ExtractType<T>, null | undefined>] extends [Domain]
? unknown
: never)
type OrderableAggregateValue = number | Date | bigint | string
type AggregateWrapper<T> = RefProxy<T> | RefLeaf<T> | BasicExpression<T>
// Constrained overloads compose through supported generics; these conditional
// fallbacks validate concrete optional/nullish unions and reject unknown.
type NumericAggregateWrapperArgument<T> = AggregateArgument<
AggregateWrapper<T>,
number
>
type OrderableAggregateWrapperArgument<T> = AggregateArgument<
AggregateWrapper<T>,
OrderableAggregateValue
>
type NumericAggregateArgument<T> = AggregateArgument<T, number>
type OrderableAggregateArgument<T> = AggregateArgument<
T,
OrderableAggregateValue
>
// Helper type to determine string function return type based on input nullability
type StringFunctionReturnType<T> =
ExtractType<T> extends infer U
? U extends string | undefined | null
? BasicExpression<U>
: BasicExpression<string | undefined | null>
: BasicExpression<string | undefined | null>
// 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<T> =
ExtractType<T> extends infer U
? U extends string | Array<any> | undefined | null | number
? BasicExpression<MapToNumber<U>>
: BasicExpression<number | undefined | null>
: BasicExpression<number | undefined | null>
// Transform string/array types to number while preserving nullability
type MapToNumber<T> = T extends string | Array<any>
? number
: T extends undefined
? undefined
: T extends null
? null
: T
// Helper type for binary numeric operations.
// Runtime coalesces nullish operands to 0 for these operations, so nullable
// operands don't make the result nullable.
type BinaryNumericReturnType = BasicExpression<number>
type DivideReturnType = BasicExpression<number | null>
// Operators
export function eq<L extends ExpressionLike, R extends ExpressionLike>(
left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never),
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function eq<T>(
left: ComparisonOperand<T>,
right: ComparisonOperand<T>,
): BasicExpression<boolean>
export function eq<T extends string | number | boolean>(
left: ComparisonOperandPrimitive<T>,
right: ComparisonOperandPrimitive<T>,
): BasicExpression<boolean>
export function eq<T>(left: Aggregate<T>, right: any): BasicExpression<boolean>
export function eq(left: any, right: any): BasicExpression<boolean> {
return new Func(`eq`, [toExpression(left), toExpression(right)])
}
export function gt<L extends ExpressionLike, R extends ExpressionLike>(
left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never),
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function gt<T>(
left: ComparisonOperand<T>,
right: ComparisonOperand<T>,
): BasicExpression<boolean>
export function gt<T extends string | number>(
left: ComparisonOperandPrimitive<T>,
right: ComparisonOperandPrimitive<T>,
): BasicExpression<boolean>
export function gt<T>(left: Aggregate<T>, right: any): BasicExpression<boolean>
export function gt(left: any, right: any): BasicExpression<boolean> {
return new Func(`gt`, [toExpression(left), toExpression(right)])
}
export function gte<L extends ExpressionLike, R extends ExpressionLike>(
left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never),
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function gte<T>(
left: ComparisonOperand<T>,
right: ComparisonOperand<T>,
): BasicExpression<boolean>
export function gte<T extends string | number>(
left: ComparisonOperandPrimitive<T>,
right: ComparisonOperandPrimitive<T>,
): BasicExpression<boolean>
export function gte<T>(left: Aggregate<T>, right: any): BasicExpression<boolean>
export function gte(left: any, right: any): BasicExpression<boolean> {
return new Func(`gte`, [toExpression(left), toExpression(right)])
}
export function lt<L extends ExpressionLike, R extends ExpressionLike>(
left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never),
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function lt<T>(
left: ComparisonOperand<T>,
right: ComparisonOperand<T>,
): BasicExpression<boolean>
export function lt<T extends string | number>(
left: ComparisonOperandPrimitive<T>,
right: ComparisonOperandPrimitive<T>,
): BasicExpression<boolean>
export function lt<T>(left: Aggregate<T>, right: any): BasicExpression<boolean>
export function lt(left: any, right: any): BasicExpression<boolean> {
return new Func(`lt`, [toExpression(left), toExpression(right)])
}
export function lte<L extends ExpressionLike, R extends ExpressionLike>(
left: L & (true extends ContainsAggregate<[L, R]> ? unknown : never),
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function lte<T>(
left: ComparisonOperand<T>,
right: ComparisonOperand<T>,
): BasicExpression<boolean>
export function lte<T extends string | number>(
left: ComparisonOperandPrimitive<T>,
right: ComparisonOperandPrimitive<T>,
): BasicExpression<boolean>
export function lte<T>(left: Aggregate<T>, right: any): BasicExpression<boolean>
export function lte(left: any, right: any): BasicExpression<boolean> {
return new Func(`lte`, [toExpression(left), toExpression(right)])
}
// Overloads for and() - support 2 or more arguments
export function and<
T extends [ExpressionLike, ExpressionLike, ...Array<ExpressionLike>],
>(...args: T): WithAggregate<BasicExpression<boolean>, T>
export function and(
left: ExpressionLike,
right: ExpressionLike,
...rest: Array<ExpressionLike>
): BasicExpression<boolean> {
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<
T extends [ExpressionLike, ExpressionLike, ...Array<ExpressionLike>],
>(...args: T): WithAggregate<BasicExpression<boolean>, T>
export function or(
left: ExpressionLike,
right: ExpressionLike,
...rest: Array<ExpressionLike>
): BasicExpression<boolean> {
const allArgs = [left, right, ...rest]
return new Func(
`or`,
allArgs.map((arg) => toExpression(arg)),
)
}
export function not<T extends ExpressionLike>(
value: T,
): WithAggregate<BasicExpression<boolean>, T>
export function not(value: ExpressionLike): BasicExpression<boolean> {
return new Func(`not`, [toExpression(value)])
}
// Null/undefined checking functions
export function isUndefined<T extends ExpressionLike>(
value: T,
): WithAggregate<BasicExpression<boolean>, T>
export function isUndefined(value: ExpressionLike): BasicExpression<boolean> {
return new Func(`isUndefined`, [toExpression(value)])
}
export function isNull<T extends ExpressionLike>(
value: T,
): WithAggregate<BasicExpression<boolean>, T>
export function isNull(value: ExpressionLike): BasicExpression<boolean> {
return new Func(`isNull`, [toExpression(value)])
}
export function inArray<L extends ExpressionLike, R extends ExpressionLike>(
left: L,
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function inArray(
value: ExpressionLike,
array: ExpressionLike,
): BasicExpression<boolean> {
return new Func(`in`, [toExpression(value), toExpression(array)])
}
export function like<L extends StringLike, R extends StringLike>(
left: L,
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function like(
left: StringLike,
right: StringLike,
): BasicExpression<boolean>
export function like(left: any, right: any): BasicExpression<boolean> {
return new Func(`like`, [toExpression(left), toExpression(right)])
}
export function ilike<L extends StringLike, R extends StringLike>(
left: L,
right: R,
): WithAggregate<BasicExpression<boolean>, [L, R]>
export function ilike(
left: StringLike,
right: StringLike,
): BasicExpression<boolean> {
return new Func(`ilike`, [toExpression(left), toExpression(right)])
}
// Functions
export function upper<T extends ExpressionLike>(
arg: T,
): WithAggregate<StringFunctionReturnType<T>, T> {
return new Func(`upper`, [toExpression(arg)]) as WithAggregate<
StringFunctionReturnType<T>,
T
>
}
export function lower<T extends ExpressionLike>(
arg: T,
): WithAggregate<StringFunctionReturnType<T>, T> {
return new Func(`lower`, [toExpression(arg)]) as WithAggregate<
StringFunctionReturnType<T>,
T
>
}
export function length<T extends ExpressionLike>(
arg: T,
): WithAggregate<NumericFunctionReturnType<T>, T> {
return new Func(`length`, [toExpression(arg)]) as WithAggregate<
NumericFunctionReturnType<T>,
T
>
}
export function concat<T extends StringifiableScalar>(
arg: ToArrayWrapper<T>,
): ConcatToArrayWrapper<T>
export function concat<T extends Array<ExpressionLike>>(
...args: T
): WithAggregate<BasicExpression<string>, T>
export function concat(
...args: Array<ExpressionLike | ToArrayWrapper<any>>
): BasicExpression<string> | ConcatToArrayWrapper<any> {
const toArrayArg = args.find(
(arg): arg is ToArrayWrapper<any> => arg instanceof ToArrayWrapper,
)
if (toArrayArg) {
if (args.length !== 1) {
throw new Error(
devBuild() && process.env.NODE_ENV !== `production`
? `concat(toArray(...)) currently supports only a single toArray(...) argument`
: codedMessage(110),
)
}
return new ConcatToArrayWrapper(toArrayArg.query, toArrayArg.runtime)
}
return new Func(
`concat`,
args.map((arg) => toExpression(arg)),
)
}
// Helper type for coalesce: extracts non-nullish value types from all args
type CoalesceArgTypes<T extends Array<ExpressionLike>> = {
[K in keyof T]: NonNullable<ExtractType<T[K]>>
}[number]
// Whether any arg in the tuple is statically guaranteed non-null (i.e., does not include null | undefined)
type HasGuaranteedNonNull<T extends Array<ExpressionLike>> = {
[K in keyof T]: null extends ExtractType<T[K]>
? false
: undefined extends ExtractType<T[K]>
? false
: true
}[number] extends false
? false
: true
// coalesce() return type: union of all non-null arg types; null included unless a guaranteed non-null arg exists
type CoalesceReturnType<T extends Array<ExpressionLike>> = WithAggregate<
HasGuaranteedNonNull<T> extends true
? BasicExpression<CoalesceArgTypes<T>>
: BasicExpression<CoalesceArgTypes<T> | null>,
T
>
export function coalesce<T extends [ExpressionLike, ...Array<ExpressionLike>]>(
...args: T
): CoalesceReturnType<T> {
return new Func(
`coalesce`,
args.map((arg) => toExpression(arg)),
) as CoalesceReturnType<T>
}
/**
* Returns the value for the first matching condition, similar to SQL
* `CASE WHEN`.
*
* Arguments are evaluated as condition/value pairs followed by an optional
* default value. Scalar branch values return a query expression and can be used
* in expression contexts like `select`, `where`, `orderBy`, `groupBy`,
* `having`, and equality join operands. If no scalar branch matches and no
* default is provided, the result is `null`.
*
* When a branch value is a projection object, `caseWhen` becomes a select-only
* projection value. Projection branches can include nested fields, ref spreads,
* and includes. If no projection branch matches and no default is provided, the
* result is `undefined`.
*
* @example
* ```ts
* caseWhen(gt(user.age, 18), `adult`, `minor`)
* ```
*
* @example
* ```ts
* caseWhen(
* gt(user.age, 65),
* `senior`,
* gt(user.age, 18),
* `adult`,
* `minor`,
* )
* ```
*
* @example
* ```ts
* caseWhen(gt(user.age, 18), {
* ...user,
* posts: include(
* q
* .from({ post: postsCollection })
* .where(({ post }) => eq(post.userId, user.id)),
* ),
* })
* ```
*/
export function caseWhen<
T extends [ExpressionLike, CaseWhenValue, ...Array<CaseWhenValue>],
>(...args: T & CaseWhenInputs<T>): CaseWhenOutput<T>
export function caseWhen(...args: Array<CaseWhenValue>): any {
if (args.length < 2) {
throw new Error(
devBuild() && process.env.NODE_ENV !== `production`
? `caseWhen() requires at least two arguments`
: codedMessage(111),
)
}
const pairCount = Math.floor(args.length / 2)
for (let i = 0; i < pairCount; i++) {
const condition = args[i * 2]
if (!isConditionValue(condition)) {
throw new Error(
devBuild() && process.env.NODE_ENV !== `production`
? `caseWhen() conditions must be expression-like values`
: codedMessage(112),
)
}
}
if (caseWhenHasOnlyExpressionValues(args)) {
return new Func(
`caseWhen`,
args.map((arg) => toExpression(arg)),
)
}
return new CaseWhenWrapper(args)
}
export function add<T1 extends ExpressionLike, T2 extends ExpressionLike>(
left: T1,
right: T2,
): WithAggregate<BinaryNumericReturnType, [T1, T2]> {
return new Func(`add`, [
toExpression(left),
toExpression(right),
]) as WithAggregate<BinaryNumericReturnType, [T1, T2]>
}
export function subtract<T1 extends ExpressionLike, T2 extends ExpressionLike>(
left: T1,
right: T2,
): WithAggregate<BinaryNumericReturnType, [T1, T2]> {
return new Func(`subtract`, [
toExpression(left),
toExpression(right),
]) as WithAggregate<BinaryNumericReturnType, [T1, T2]>
}
export function multiply<T1 extends ExpressionLike, T2 extends ExpressionLike>(
left: T1,
right: T2,
): WithAggregate<BinaryNumericReturnType, [T1, T2]> {
return new Func(`multiply`, [
toExpression(left),
toExpression(right),
]) as WithAggregate<BinaryNumericReturnType, [T1, T2]>
}
export function divide<T1 extends ExpressionLike, T2 extends ExpressionLike>(
left: T1,
right: T2,
): WithAggregate<DivideReturnType, [T1, T2]> {
return new Func(`divide`, [
toExpression(left),
toExpression(right),
]) as WithAggregate<DivideReturnType, [T1, T2]>
}
// Aggregates
export function count(arg: ExpressionLike): Aggregate<number> {
return new Aggregate(`count`, [toExpression(arg)], grouping)
}
export function avg<T extends number>(arg: T): Aggregate<number>
export function avg<T>(
arg: NumericAggregateWrapperArgument<T>,
): Aggregate<number>
export function avg<T extends ExpressionLike>(
arg: NumericAggregateArgument<T>,
): Aggregate<number>
export function avg(arg: ExpressionLike): Aggregate<number> {
return new Aggregate(`avg`, [toExpression(arg)], grouping)
}
export function sum<T extends number>(arg: T): Aggregate<number>
export function sum<T>(
arg: NumericAggregateWrapperArgument<T>,
): Aggregate<number>
export function sum<T extends ExpressionLike>(
arg: NumericAggregateArgument<T>,
): Aggregate<number>
export function sum(arg: ExpressionLike): Aggregate<number> {
return new Aggregate(`sum`, [toExpression(arg)], grouping)
}
export function min<T extends OrderableAggregateValue>(arg: T): Aggregate<T>
export function min<T>(arg: OrderableAggregateWrapperArgument<T>): Aggregate<T>
export function min<T extends ExpressionLike>(
arg: OrderableAggregateArgument<T>,
): Aggregate<ExtractType<T>>
export function min(arg: ExpressionLike): Aggregate {
return new Aggregate(`min`, [toExpression(arg)], grouping)
}
export function max<T extends OrderableAggregateValue>(arg: T): Aggregate<T>
export function max<T>(arg: OrderableAggregateWrapperArgument<T>): Aggregate<T>
export function max<T extends ExpressionLike>(
arg: OrderableAggregateArgument<T>,
): Aggregate<ExtractType<T>>
export function max(arg: ExpressionLike): Aggregate {
return new Aggregate(`max`, [toExpression(arg)], grouping)
}
/**
* List of comparison function names that can be used with indexes
*/
export const indexableOperations = [
`eq`,
`gt`,
`gte`,
`lt`,
`lte`,
`in`,
] as const
export const comparisonFunctions = [
...indexableOperations,
`like`,
`ilike`,
] as const
/**
* All supported operator names in TanStack DB expressions
*/
export const operators = [
// Comparison operators
`eq`,
`gt`,
`gte`,
`lt`,
`lte`,
`in`,
`like`,
`ilike`,
// Logical operators
`and`,
`or`,
`not`,
// Null checking
`isNull`,
`isUndefined`,
// String functions
`upper`,
`lower`,
`length`,
`concat`,
// Numeric functions
`add`,
`subtract`,
`multiply`,
`divide`,
// Utility functions
`coalesce`,
`caseWhen`,
// Aggregate functions
`count`,
`avg`,
`sum`,
`min`,
`max`,
] as const
export type OperatorName = (typeof operators)[number]
export class ToArrayWrapper<_T = unknown> {
readonly __brand = `ToArrayWrapper` as const
declare readonly _type: `toArray`
declare readonly _result: _T
constructor(
public readonly query: QueryBuilder<any>,
public readonly runtime: IncludesRuntime,
) {
registerWrapper(this, `toArray()`)
}
}
export class ConcatToArrayWrapper<_T = unknown> {
readonly __brand = `ConcatToArrayWrapper` as const
declare readonly _type: `concatToArray`
declare readonly _result: _T
constructor(
public readonly query: QueryBuilder<any>,
public readonly runtime: IncludesRuntime,
) {
registerWrapper(this, `concat(toArray())`)
}
}
export class CaseWhenWrapper<_T = any> {
readonly __brand = `CaseWhenWrapper` as const
declare readonly _type: `caseWhen`
readonly _result?: _T
constructor(public readonly args: Array<CaseWhenValue>) {
registerWrapper(this, `caseWhen()`)
}
}
export class MaterializeWrapper<
_T = unknown,
_IsSingle extends boolean = boolean,
> {
readonly __brand = `MaterializeWrapper` as const
declare readonly _type: `materialize`
declare readonly _result: _T
declare readonly _isSingle: _IsSingle
constructor(
public readonly query: QueryBuilder<any>,
public readonly runtime: IncludesRuntime,
) {
registerWrapper(this, `materialize()`)
}
}
function caseWhenHasOnlyExpressionValues(args: Array<CaseWhenValue>): boolean {
const valueIndexes = getCaseWhenValueIndexes(args.length)
return valueIndexes.every((index) => isExpressionValue(args[index]))
}
function getCaseWhenValueIndexes(argCount: number): Array<number> {
const valueIndexes: Array<number> = []
const hasDefaultValue = argCount % 2 === 1
const pairCount = Math.floor(argCount / 2)
for (let i = 0; i < pairCount; i++) {
valueIndexes.push(i * 2 + 1)
}
if (hasDefaultValue) {
valueIndexes.push(argCount - 1)
}
return valueIndexes
}
function isExpressionValue(value: CaseWhenValue | undefined): boolean {
if (isRefProxy(value)) return true
if (isBasicOrAggregateExpression(value)) return true
if (value == null) return true
if (
typeof value === `string` ||
typeof value === `number` ||
typeof value === `boolean` ||
typeof value === `bigint`
) {
return true
}
if (value instanceof Date || Array.isArray(value)) return true
return false
}
function isConditionValue(value: CaseWhenValue | undefined): boolean {
return isExpressionValue(value) && !Array.isArray(value)
}
export class IncludeWrapper<_TContext extends Context = any> {
readonly __brand = `IncludeWrapper` as const
declare readonly _context: _TContext
constructor(
public readonly query: BaseQueryBuilder<any>,
public readonly runtime: IncludesRuntime,
) {
registerWrapper(this, `include()`)
}
}