UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

207 lines (206 loc) 8.28 kB
import { groupedOrderByWithFractionalIndex, orderByWithFractionalIndex } from "@tanstack/db-ivm"; import { makeComparator, defaultComparator } from "../../utils/comparison.js"; import { followRef, collectCollectionSources, PropRef, isResidualWhere, getWhereExpression } from "../ir.js"; import { ensureIndexForField } from "../../indexes/auto-index.js"; import { findIndexForField } from "../../utils/index-optimization.js"; import { compileExpression } from "./evaluators.js"; import { getSourceAliasesFromExpression } from "./expressions.js"; import { replaceAggregatesByRefs } from "./group-by.js"; function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collection, optimizableOrderByCollections, setWindowFn, limit, offset, groupKeyFn) { const compiledOrderBy = orderByClause.map((clause) => { const clauseWithoutAggregates = replaceAggregatesByRefs( clause.expression, selectClause, `$selected` ); return { compiledExpression: compileExpression(clauseWithoutAggregates), compareOptions: buildCompareOptions(clause, collection) }; }); const valueExtractor = (row) => { const orderByContext = row; if (orderByClause.length > 1) { return compiledOrderBy.map( (compiled) => compiled.compiledExpression(orderByContext) ); } else if (orderByClause.length === 1) { const compiled = compiledOrderBy[0]; return compiled.compiledExpression(orderByContext); } return null; }; const compare = (a, b) => { if (orderByClause.length > 1) { const arrayA = a; const arrayB = b; for (let i = 0; i < orderByClause.length; i++) { const clause = compiledOrderBy[i]; const compareFn = makeComparator(clause.compareOptions); const result = compareFn(arrayA[i], arrayB[i]); if (result !== 0) { return result; } } return arrayA.length - arrayB.length; } if (orderByClause.length === 1) { const clause = compiledOrderBy[0]; const compareFn = makeComparator(clause.compareOptions); return compareFn(a, b); } return defaultComparator(a, b); }; let setSizeCallback; let orderByOptimizationInfo; if (limit !== void 0 && !groupKeyFn && rawQuery.from.type !== `unionFrom` && rawQuery.from.type !== `unionAll`) { let index; let followRefCollection; let orderByAlias = rawQuery.from.alias; let orderBySourceId; const firstClause = orderByClause[0]; const firstOrderByExpression = firstClause.expression; const followRefResult = firstOrderByExpression.type === `ref` ? followRef(rawQuery, firstOrderByExpression, collection) : void 0; if (firstOrderByExpression.type === `ref` && followRefResult) { followRefCollection = followRefResult.collection; orderBySourceId = followRefResult.sourceId; const fieldName = followRefResult.path[0]; const compareOpts = buildCompareOptions(firstClause, collection); if (fieldName) { const firstColumnCompareFn = makeComparator(compareOpts); ensureIndexForField( fieldName, followRefResult.path, followRefCollection, compareOpts, firstColumnCompareFn ); } index = findIndexForField( followRefCollection, followRefResult.path, compareOpts ); if (!index?.supports(`gt`)) { index = void 0; } if (!index) { const collectionId = followRefCollection.id; const fieldPath = followRefResult.path.join(`.`); console.warn( `[TanStack DB]${collectionId ? ` [${collectionId}]` : ``} orderBy with limit requires an index on "${fieldPath}" for efficient lazy loading. Falling back to loading all data. Consider creating an index on the collection with collection.createIndex((row) => row.${fieldPath}) or enable auto-indexing with autoIndex: 'eager' and a defaultIndexType.` ); } orderByAlias = firstOrderByExpression.path.length > 1 ? String(firstOrderByExpression.path[0]) : rawQuery.from.alias; orderBySourceId ??= collectCollectionSources(rawQuery).find( (source) => source.alias === orderByAlias && source.collection === followRefCollection )?.sourceId; } if (orderBySourceId && followRefResult) { const sourceOrderBy = resolveOrderBy( orderByClause, collection.compareOptions ); const sourceOrderIsDirect = orderByClause.every(({ expression }) => { if (expression.type !== `ref`) return false; return followRef(rawQuery, expression, collection)?.sourceId === orderBySourceId; }); const extract = compileExpression( new PropRef(followRefResult.path), true ); const compareTerm = makeComparator(sourceOrderBy[0].compareOptions); const compareSourceRows = (a, b) => compareTerm(a ? extract(a) : a, b ? extract(b) : b); const info = { sourceId: orderBySourceId, alias: orderByAlias, offset: offset ?? 0, limit, comparator: compareSourceRows, valueExtractorForRawRow: extract, index, orderBy: sourceOrderBy, requiresFullSource: !sourceOrderIsDirect || rawQuery.from.type !== `collectionRef` || rawQuery.from.sourceId !== orderBySourceId || (rawQuery.join?.some( ({ type }) => type === `inner` || type === `right` ) ?? false) || (rawQuery.where?.some( (where) => isResidualWhere(where) || [ ...getSourceAliasesFromExpression(getWhereExpression(where)) ].some((alias) => alias !== orderByAlias) ) ?? false) || (rawQuery.fnWhere?.length ?? 0) > 0 || rawQuery.groupBy !== void 0 || rawQuery.having !== void 0 || rawQuery.fnHaving !== void 0 || rawQuery.distinct === true }; orderByOptimizationInfo = info; optimizableOrderByCollections[orderBySourceId] = info; if (index) { setSizeCallback = (getSize) => { optimizableOrderByCollections[orderBySourceId][`dataNeeded`] = () => { const size = getSize(); return Math.max(0, info.limit - size); }; }; } } } if (groupKeyFn) { return pipeline.pipe( groupedOrderByWithFractionalIndex(valueExtractor, { limit, offset, comparator: compare, setSizeCallback, groupKeyFn, setWindowFn: (windowFn) => { setWindowFn((options) => { windowFn(options); if (orderByOptimizationInfo) { orderByOptimizationInfo.offset = options.offset ?? orderByOptimizationInfo.offset; orderByOptimizationInfo.limit = options.limit ?? orderByOptimizationInfo.limit; } }); } }) ); } return pipeline.pipe( orderByWithFractionalIndex(valueExtractor, { limit, offset, comparator: compare, setSizeCallback, setWindowFn: (windowFn) => { setWindowFn( // We wrap the move function such that we update the orderByOptimizationInfo // because that is used by the `dataNeeded` callback to determine if we need to load more data (options) => { windowFn(options); if (orderByOptimizationInfo) { orderByOptimizationInfo.offset = options.offset ?? orderByOptimizationInfo.offset; orderByOptimizationInfo.limit = options.limit ?? orderByOptimizationInfo.limit; } } ); } }) // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format ); } function buildCompareOptions(clause, collection) { return resolveCompareOptions(clause, collection.compareOptions); } function resolveOrderBy(orderBy, defaults) { return orderBy.map((clause) => ({ expression: clause.expression, compareOptions: resolveCompareOptions(clause, defaults) })); } function resolveCompareOptions(clause, defaults) { return clause.compareOptions.stringSort === void 0 ? { ...defaults, direction: clause.compareOptions.direction, nulls: clause.compareOptions.nulls } : clause.compareOptions; } export { buildCompareOptions, processOrderBy }; //# sourceMappingURL=order-by.js.map