UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1 lines 19.8 kB
{"version":3,"file":"order-by.cjs","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import {\n groupedOrderByWithFractionalIndex,\n orderByWithFractionalIndex,\n} from '@tanstack/db-ivm'\nimport { defaultComparator, makeComparator } from '../../utils/comparison.js'\nimport {\n PropRef,\n collectCollectionSources,\n followRef,\n getWhereExpression,\n isResidualWhere,\n} from '../ir.js'\nimport { ensureIndexForField } from '../../indexes/auto-index.js'\nimport { findIndexForField } from '../../utils/index-optimization.js'\nimport { compileExpression } from './evaluators.js'\nimport { getSourceAliasesFromExpression } from './expressions.js'\nimport { replaceAggregatesByRefs } from './group-by.js'\nimport type { CompareOptions } from '../builder/types.js'\nimport type { WindowOptions } from './types.js'\nimport type { CompiledSingleRowExpression } from './evaluators.js'\nimport type { OrderBy, OrderByClause, QueryIR, Select } from '../ir.js'\nimport type {\n CollectionLike,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from '../../types.js'\nimport type { IStreamBuilder, KeyValue } from '@tanstack/db-ivm'\nimport type { IndexReader } from '../../indexes/base-index.js'\nimport type { Collection } from '../../collection/index.js'\n\nexport type OrderByOptimizationInfo = {\n sourceId: string\n alias: string\n orderBy: OrderBy\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined,\n ) => number\n /** Extracts all orderBy column values from a raw row (array for multi-column) */\n valueExtractorForRawRow: (row: Record<string, unknown>) => unknown\n /** Index on the first orderBy column - used for lazy loading */\n index?: IndexReader<string | number>\n dataNeeded?: () => number\n /** Reads the source loader's synchronous request guard, when installed. */\n isRequesting?: () => boolean\n /** Whether local operators can discard or reorder the provider's prefix. */\n requiresFullSource: boolean\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and $selected\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n limit?: number,\n offset?: number,\n groupKeyFn?: (key: unknown, value: unknown) => unknown,\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `$selected`,\n )\n\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: buildCompareOptions(clause, collection),\n }\n })\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { $selected?: any }) => {\n // The namespaced row contains:\n // 1. Table aliases as top-level properties (e.g., row[\"tableName\"])\n // 2. SELECT results in $selected (e.g., row.$selected[\"aggregateAlias\"])\n // The replaceAggregatesByRefs function has already transformed:\n // - Aggregate expressions that match SELECT aggregates to use the $selected namespace\n // - $selected ref expressions are passed through unchanged (already using the correct namespace)\n const orderByContext = row\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext),\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = compiledOrderBy[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = compiledOrderBy[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n let orderByOptimizationInfo: OrderByOptimizationInfo | undefined\n\n // When there's a limit, we create orderByOptimizationInfo to pass orderBy/limit\n // to loadSubset so the sync layer can optimize the query.\n // We try to use an index on the FIRST orderBy column for lazy loading,\n // even for multi-column orderBy (using wider bounds on first column).\n // Skip this optimization when using grouped ordering (includes with limit),\n // because the limit is per-group, not global — the child collection needs all data loaded.\n if (\n limit !== undefined &&\n !groupKeyFn &&\n rawQuery.from.type !== `unionFrom` &&\n rawQuery.from.type !== `unionAll`\n ) {\n let index: IndexReader<string | number> | undefined\n let followRefCollection: Collection | undefined\n let orderByAlias: string = rawQuery.from.alias\n let orderBySourceId: string | undefined\n\n // Try to create/find an index on the FIRST orderBy column for lazy loading\n const firstClause = orderByClause[0]!\n const firstOrderByExpression = firstClause.expression\n\n const followRefResult =\n firstOrderByExpression.type === `ref`\n ? followRef(rawQuery, firstOrderByExpression, collection)\n : undefined\n if (firstOrderByExpression.type === `ref` && followRefResult) {\n followRefCollection = followRefResult.collection\n orderBySourceId = followRefResult.sourceId\n const fieldName = followRefResult.path[0]\n // The query's first source defines implicit string collation for the\n // whole order. Build the source index with that same resolved term so\n // provider admission cannot disagree with emitted query order.\n const compareOpts = buildCompareOptions(firstClause, collection)\n\n if (fieldName) {\n // Use a single-column comparator for the index, not the\n // multi-column `compare` function. The multi-column comparator\n // expects array values [col1, col2, ...] but the index stores\n // individual field values. Passing `compare` here causes the\n // BTree to treat all single values as equal (since number[0]\n // === undefined for both sides of the comparison).\n const firstColumnCompareFn = makeComparator(compareOpts)\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n compareOpts,\n firstColumnCompareFn,\n )\n }\n\n index = findIndexForField(\n followRefCollection,\n followRefResult.path,\n compareOpts,\n )\n\n // Only use the index if it supports range queries\n if (!index?.supports(`gt`)) {\n index = undefined\n }\n\n if (!index) {\n const collectionId = followRefCollection.id\n const fieldPath = followRefResult.path.join(`.`)\n console.warn(\n `[TanStack DB]${collectionId ? ` [${collectionId}]` : ``} orderBy with limit requires an index on \"${fieldPath}\" for efficient lazy loading. ` +\n `Falling back to loading all data. ` +\n `Consider creating an index on the collection with collection.createIndex((row) => row.${fieldPath}) ` +\n `or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.`,\n )\n }\n\n orderByAlias =\n firstOrderByExpression.path.length > 1\n ? String(firstOrderByExpression.path[0])\n : rawQuery.from.alias\n orderBySourceId ??= collectCollectionSources(rawQuery).find(\n (source) =>\n source.alias === orderByAlias &&\n source.collection === followRefCollection,\n )?.sourceId\n }\n\n if (orderBySourceId && followRefResult) {\n const sourceOrderBy = resolveOrderBy(\n orderByClause,\n collection.compareOptions,\n )\n const sourceOrderIsDirect = orderByClause.every(({ expression }) => {\n if (expression.type !== `ref`) return false\n return (\n followRef(rawQuery, expression, collection)?.sourceId ===\n orderBySourceId\n )\n })\n const extract = compileExpression(\n new PropRef(followRefResult.path),\n true,\n ) as CompiledSingleRowExpression\n const compareTerm = makeComparator(sourceOrderBy[0]!.compareOptions)\n const compareSourceRows = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined,\n ) => compareTerm(a ? extract(a) : a, b ? extract(b) : b)\n\n const info: OrderByOptimizationInfo = {\n sourceId: orderBySourceId,\n alias: orderByAlias,\n offset: offset ?? 0,\n limit,\n comparator: compareSourceRows,\n valueExtractorForRawRow: extract,\n index,\n orderBy: sourceOrderBy,\n requiresFullSource:\n !sourceOrderIsDirect ||\n rawQuery.from.type !== `collectionRef` ||\n rawQuery.from.sourceId !== orderBySourceId ||\n (rawQuery.join?.some(\n ({ type }) => type === `inner` || type === `right`,\n ) ??\n false) ||\n (rawQuery.where?.some(\n (where) =>\n isResidualWhere(where) ||\n [\n ...getSourceAliasesFromExpression(getWhereExpression(where)),\n ].some((alias) => alias !== orderByAlias),\n ) ??\n false) ||\n (rawQuery.fnWhere?.length ?? 0) > 0 ||\n rawQuery.groupBy !== undefined ||\n rawQuery.having !== undefined ||\n rawQuery.fnHaving !== undefined ||\n rawQuery.distinct === true,\n }\n orderByOptimizationInfo = info\n\n // Ordered loading is owned by one lexical source. A collection can occur\n // more than once in a query tree, so collection ID and alias are not\n // sufficient identities here.\n optimizableOrderByCollections[orderBySourceId] = info\n\n // Set up lazy loading callback to track how much more data is needed\n // This is used by loadMoreIfNeeded to determine if more data should be loaded\n // Only enable when an index exists — without an index, lazy loading can't work\n // and all data is loaded eagerly via requestSnapshot instead.\n if (index) {\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[orderBySourceId]![`dataNeeded`] =\n () => {\n const size = getSize()\n return Math.max(0, info.limit - size)\n }\n }\n }\n }\n }\n\n // Use grouped ordering when a groupKeyFn is provided (includes with limit/offset),\n // otherwise use the standard global ordering operator.\n if (groupKeyFn) {\n return pipeline.pipe(\n groupedOrderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n groupKeyFn,\n setWindowFn: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => {\n setWindowFn((options) => {\n windowFn(options)\n if (orderByOptimizationInfo) {\n orderByOptimizationInfo.offset =\n options.offset ?? orderByOptimizationInfo.offset\n orderByOptimizationInfo.limit =\n options.limit ?? orderByOptimizationInfo.limit\n }\n })\n },\n }),\n )\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n setWindowFn: (\n windowFn: (options: { offset?: number; limit?: number }) => void,\n ) => {\n setWindowFn(\n // We wrap the move function such that we update the orderByOptimizationInfo\n // because that is used by the `dataNeeded` callback to determine if we need to load more data\n (options) => {\n windowFn(options)\n if (orderByOptimizationInfo) {\n orderByOptimizationInfo.offset =\n options.offset ?? orderByOptimizationInfo.offset\n orderByOptimizationInfo.limit =\n options.limit ?? orderByOptimizationInfo.limit\n }\n },\n )\n },\n }),\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n\n/**\n * Builds a comparison configuration object that uses the values provided in the orderBy clause.\n * If no string sort configuration is provided it defaults to the collection's string sort configuration.\n * Multi-source FROM queries pass their first source collection here as the\n * documented default. Use explicit orderBy compare options when branches need\n * different string collation behavior.\n */\nexport function buildCompareOptions(\n clause: OrderByClause,\n collection: CollectionLike<any, any>,\n): CompareOptions {\n return resolveCompareOptions(clause, collection.compareOptions)\n}\n\nfunction resolveOrderBy(\n orderBy: OrderBy,\n defaults: CollectionLike[`compareOptions`],\n): OrderBy {\n return orderBy.map((clause) => ({\n expression: clause.expression,\n compareOptions: resolveCompareOptions(clause, defaults),\n }))\n}\n\nfunction resolveCompareOptions(\n clause: OrderByClause,\n defaults: CollectionLike[`compareOptions`],\n): CompareOptions {\n return clause.compareOptions.stringSort === undefined\n ? {\n ...defaults,\n direction: clause.compareOptions.direction,\n nulls: clause.compareOptions.nulls,\n }\n : clause.compareOptions\n}\n"],"names":["replaceAggregatesByRefs","compileExpression","makeComparator","defaultComparator","followRef","ensureIndexForField","findIndexForField","collectCollectionSources","PropRef","isResidualWhere","getSourceAliasesFromExpression","getWhereExpression","groupedOrderByWithFractionalIndex","orderByWithFractionalIndex"],"mappings":"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"}