@tanstack/db
Version:
A reactive client store for building super fast apps on sync
258 lines (220 loc) • 6.34 kB
text/typescript
/*
This is the intermediate representation of the query.
*/
import type { CompareOptions } from './builder/types'
import type { Collection, CollectionImpl } from '../collection/index.js'
import type { NamespacedRow } from '../types'
export interface QueryIR {
from: From
select?: Select
join?: Join
where?: Array<Where>
groupBy?: GroupBy
having?: Array<Having>
orderBy?: OrderBy
limit?: Limit
offset?: Offset
distinct?: true
singleResult?: true
// Functional variants
fnSelect?: (row: NamespacedRow) => any
fnWhere?: Array<(row: NamespacedRow) => any>
fnHaving?: Array<(row: NamespacedRow) => any>
}
export type From = CollectionRef | QueryRef
export type Select = {
[alias: string]: BasicExpression | Aggregate | Select
}
export type Join = Array<JoinClause>
export interface JoinClause {
from: CollectionRef | QueryRef
type: `left` | `right` | `inner` | `outer` | `full` | `cross`
left: BasicExpression
right: BasicExpression
}
export type Where =
| BasicExpression<boolean>
| { expression: BasicExpression<boolean>; residual?: boolean }
export type GroupBy = Array<BasicExpression>
export type Having = Where
export type OrderBy = Array<OrderByClause>
export type OrderByClause = {
expression: BasicExpression
compareOptions: CompareOptions
}
export type OrderByDirection = `asc` | `desc`
export type Limit = number
export type Offset = number
/* Expressions */
abstract class BaseExpression<T = any> {
public abstract type: string
/** @internal - Type brand for TypeScript inference */
declare readonly __returnType: T
}
export class CollectionRef extends BaseExpression {
public type = `collectionRef` as const
constructor(
public collection: CollectionImpl,
public alias: string,
) {
super()
}
}
export class QueryRef extends BaseExpression {
public type = `queryRef` as const
constructor(
public query: QueryIR,
public alias: string,
) {
super()
}
}
export class PropRef<T = any> extends BaseExpression<T> {
public type = `ref` as const
constructor(
public path: Array<string>, // path to the property in the collection, with the alias as the first element
) {
super()
}
}
export class Value<T = any> extends BaseExpression<T> {
public type = `val` as const
constructor(
public value: T, // any js value
) {
super()
}
}
export class Func<T = any> extends BaseExpression<T> {
public type = `func` as const
constructor(
public name: string, // such as eq, gt, lt, upper, lower, etc.
public args: Array<BasicExpression>,
) {
super()
}
}
// This is the basic expression type that is used in the majority of expression
// builder callbacks (select, where, groupBy, having, orderBy, etc.)
// it doesn't include aggregate functions as those are only used in the select clause
export type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>
export class Aggregate<T = any> extends BaseExpression<T> {
public type = `agg` as const
constructor(
public name: string, // such as count, avg, sum, min, max, etc.
public args: Array<BasicExpression>,
) {
super()
}
}
/**
* Runtime helper to detect IR expression-like objects.
* Prefer this over ad-hoc local implementations to keep behavior consistent.
*/
export function isExpressionLike(value: any): boolean {
return (
value instanceof Aggregate ||
value instanceof Func ||
value instanceof PropRef ||
value instanceof Value
)
}
/**
* Helper functions for working with Where clauses
*/
/**
* Extract the expression from a Where clause
*/
export function getWhereExpression(where: Where): BasicExpression<boolean> {
return typeof where === `object` && `expression` in where
? where.expression
: where
}
/**
* Extract the expression from a HAVING clause
* HAVING clauses can contain aggregates, unlike regular WHERE clauses
*/
export function getHavingExpression(
having: Having,
): BasicExpression | Aggregate {
return typeof having === `object` && `expression` in having
? having.expression
: having
}
/**
* Check if a Where clause is marked as residual
*/
export function isResidualWhere(where: Where): boolean {
return (
typeof where === `object` &&
`expression` in where &&
where.residual === true
)
}
/**
* Create a residual Where clause from an expression
*/
export function createResidualWhere(
expression: BasicExpression<boolean>,
): Where {
return { expression, residual: true }
}
function getRefFromAlias(
query: QueryIR,
alias: string,
): CollectionRef | QueryRef | void {
if (query.from.alias === alias) {
return query.from
}
for (const join of query.join || []) {
if (join.from.alias === alias) {
return join.from
}
}
}
/**
* Follows the given reference in a query
* until its finds the root field the reference points to.
* @returns The collection, its alias, and the path to the root field in this collection
*/
export function followRef(
query: QueryIR,
ref: PropRef<any>,
collection: Collection,
): { collection: Collection; path: Array<string> } | void {
if (ref.path.length === 0) {
return
}
if (ref.path.length === 1) {
// This field should be part of this collection
const field = ref.path[0]!
// is it part of the select clause?
if (query.select) {
const selectedField = query.select[field]
if (selectedField && selectedField.type === `ref`) {
return followRef(query, selectedField, collection)
}
}
// Either this field is not part of the select clause
// and thus it must be part of the collection itself
// or it is part of the select but is not a reference
// so we can stop here and don't have to follow it
return { collection, path: [field] }
}
if (ref.path.length > 1) {
// This is a nested field
const [alias, ...rest] = ref.path
const aliasRef = getRefFromAlias(query, alias!)
if (!aliasRef) {
return
}
if (aliasRef.type === `queryRef`) {
return followRef(aliasRef.query, new PropRef(rest), collection)
} else {
// This is a reference to a collection
// we can't follow it further
// so the field must be on the collection itself
return { collection: aliasRef.collection, path: rest }
}
}
}