@tanstack/db
Version:
A reactive client store for building super fast apps on sync
79 lines (70 loc) • 2.83 kB
text/typescript
import { and, eq, gt, lt, or } from '../query/builder/functions.js'
import { Value } from '../query/ir.js'
import type { BasicExpression, OrderBy } from '../query/ir.js'
/**
* Builds a cursor expression for paginating through ordered results.
* For multi-column orderBy, creates a composite cursor that respects all columns.
*
* For [col1 ASC, col2 DESC] with values [v1, v2], produces:
* or(
* gt(col1, v1), // col1 > v1
* and(eq(col1, v1), lt(col2, v2)) // col1 = v1 AND col2 < v2 (DESC)
* )
*
* This creates a precise cursor that works with composite indexes on the backend.
*
* @param orderBy - The order-by clauses defining sort columns and directions
* @param values - The cursor values corresponding to each order-by column
* @returns A filter expression for rows after the cursor position, or undefined if empty
*/
export function buildCursor(
orderBy: OrderBy,
values: Array<unknown>,
): BasicExpression<boolean> | undefined {
if (values.length === 0 || orderBy.length === 0) {
return undefined
}
// For single column, just use simple gt/lt
if (orderBy.length === 1) {
const { expression, compareOptions } = orderBy[0]!
const operator = compareOptions.direction === `asc` ? gt : lt
return operator(expression, new Value(values[0]))
}
// For multi-column, build the composite cursor:
// or(
// gt(col1, v1),
// and(eq(col1, v1), gt(col2, v2)),
// and(eq(col1, v1), eq(col2, v2), gt(col3, v3)),
// ...
// )
const clauses: Array<BasicExpression<boolean>> = []
for (let i = 0; i < orderBy.length && i < values.length; i++) {
const clause = orderBy[i]!
const value = values[i]
// Build equality conditions for all previous columns
const eqConditions: Array<BasicExpression<boolean>> = []
for (let j = 0; j < i; j++) {
const prevClause = orderBy[j]!
const prevValue = values[j]
eqConditions.push(eq(prevClause.expression, new Value(prevValue)))
}
// Add the comparison for the current column (respecting direction)
const operator = clause.compareOptions.direction === `asc` ? gt : lt
const comparison = operator(clause.expression, new Value(value))
if (eqConditions.length === 0) {
// First column: just the comparison
clauses.push(comparison)
} else {
// Subsequent columns: and(eq(prev...), comparison)
// We need to spread into and() which expects at least 2 args
const allConditions = [...eqConditions, comparison]
clauses.push(allConditions.reduce((acc, cond) => and(acc, cond)))
}
}
// Combine all clauses with OR
if (clauses.length === 1) {
return clauses[0]!
}
// Use reduce to combine with or() which expects exactly 2 args
return clauses.reduce((acc, clause) => or(acc, clause))
}