@tanstack/db
Version:
A reactive client store for building super fast apps on sync
207 lines (206 loc) • 8.28 kB
JavaScript
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