UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

196 lines (174 loc) 6.77 kB
import { orderByWithFractionalIndex } from "@tanstack/db-ivm" import { defaultComparator, makeComparator } from "../../utils/comparison.js" import { PropRef } from "../ir.js" import { ensureIndexForField } from "../../indexes/auto-index.js" import { findIndexForField } from "../../utils/index-optimization.js" import { compileExpression } from "./evaluators.js" import { replaceAggregatesByRefs } from "./group-by.js" import { followRef } from "./index.js" import type { CompiledSingleRowExpression } from "./evaluators.js" import type { OrderByClause, QueryIR, Select } from "../ir.js" import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js" import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm" import type { BaseIndex } from "../../indexes/base-index.js" import type { Collection } from "../../collection.js" export type OrderByOptimizationInfo = { offset: number limit: number comparator: ( a: Record<string, unknown> | null | undefined, b: Record<string, unknown> | null | undefined ) => number valueExtractorForRawRow: (row: Record<string, unknown>) => any index: BaseIndex<string | number> dataNeeded?: () => number } /** * Processes the ORDER BY clause * Works with the new structure that has both namespaced row data and __select_results * Always uses fractional indexing and adds the index as __ordering_index to the result */ export function processOrderBy( rawQuery: QueryIR, pipeline: NamespacedAndKeyedStream, orderByClause: Array<OrderByClause>, selectClause: Select, collection: Collection, optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>, limit?: number, offset?: number ): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> { // Pre-compile all order by expressions const compiledOrderBy = orderByClause.map((clause) => { const clauseWithoutAggregates = replaceAggregatesByRefs( clause.expression, selectClause, `__select_results` ) return { compiledExpression: compileExpression(clauseWithoutAggregates), compareOptions: clause.compareOptions, } }) // Create a value extractor function for the orderBy operator const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => { // For ORDER BY expressions, we need to provide access to both: // 1. The original namespaced row data (for direct table column references) // 2. The __select_results (for SELECT alias references) // Create a merged context for expression evaluation const orderByContext = { ...row } // If there are select results, merge them at the top level for alias access if (row.__select_results) { // Add select results as top-level properties for alias access Object.assign(orderByContext, row.__select_results) } if (orderByClause.length > 1) { // For multiple orderBy columns, create a composite key return compiledOrderBy.map((compiled) => compiled.compiledExpression(orderByContext) ) } else if (orderByClause.length === 1) { // For a single orderBy column, use the value directly const compiled = compiledOrderBy[0]! return compiled.compiledExpression(orderByContext) } // Default case - no ordering return null } // Create a multi-property comparator that respects the order and direction of each property const compare = (a: unknown, b: unknown) => { // If we're comparing arrays (multiple properties), compare each property in order if (orderByClause.length > 1) { const arrayA = a as Array<unknown> const arrayB = b as Array<unknown> for (let i = 0; i < orderByClause.length; i++) { const clause = orderByClause[i]! const compareFn = makeComparator(clause.compareOptions) const result = compareFn(arrayA[i], arrayB[i]) if (result !== 0) { return result } } return arrayA.length - arrayB.length } // Single property comparison if (orderByClause.length === 1) { const clause = orderByClause[0]! const compareFn = makeComparator(clause.compareOptions) return compareFn(a, b) } return defaultComparator(a, b) } let setSizeCallback: ((getSize: () => number) => void) | undefined // Optimize the orderBy operator to lazily load elements // by using the range index of the collection. // Only for orderBy clause on a single column for now (no composite ordering) if (limit && orderByClause.length === 1) { const clause = orderByClause[0]! const orderByExpression = clause.expression if (orderByExpression.type === `ref`) { const followRefResult = followRef( rawQuery, orderByExpression, collection )! const followRefCollection = followRefResult.collection const fieldName = followRefResult.path[0] if (fieldName) { ensureIndexForField( fieldName, followRefResult.path, followRefCollection, compare ) } const valueExtractorForRawRow = compileExpression( new PropRef(followRefResult.path), true ) as CompiledSingleRowExpression const comparator = ( a: Record<string, unknown> | null | undefined, b: Record<string, unknown> | null | undefined ) => { const extractedA = a ? valueExtractorForRawRow(a) : a const extractedB = b ? valueExtractorForRawRow(b) : b return compare(extractedA, extractedB) } const index: BaseIndex<string | number> | undefined = findIndexForField( followRefCollection.indexes, followRefResult.path ) if (index && index.supports(`gt`)) { // We found an index that we can use to lazily load ordered data const orderByOptimizationInfo = { offset: offset ?? 0, limit, comparator, valueExtractorForRawRow, index, } optimizableOrderByCollections[followRefCollection.id] = orderByOptimizationInfo setSizeCallback = (getSize: () => number) => { optimizableOrderByCollections[followRefCollection.id] = { ...optimizableOrderByCollections[followRefCollection.id]!, dataNeeded: () => { const size = getSize() return Math.max(0, limit - size) }, } } } } } // Use fractional indexing and return the tuple [value, index] return pipeline.pipe( orderByWithFractionalIndex(valueExtractor, { limit, offset, comparator: compare, setSizeCallback, }) // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format ) }