UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

265 lines (243 loc) 8.54 kB
import { map, orderBy, orderByWithFractionalIndex, orderByWithIndex, } from "@electric-sql/d2mini" import { evaluateOperandOnNamespacedRow } from "./extractors" import { isOrderIndexFunctionCall } from "./utils" import type { ConditionOperand, Query } from "./schema" import type { KeyedNamespacedRow, NamespacedAndKeyedStream, NamespacedRow, } from "../types" type OrderByItem = { operand: ConditionOperand direction: `asc` | `desc` } type OrderByItems = Array<OrderByItem> export function processOrderBy( resultPipeline: NamespacedAndKeyedStream, query: Query, mainTableAlias: string ) { // Check if any column in the SELECT clause is an ORDER_INDEX function call let hasOrderIndexColumn = false let orderIndexType: `numeric` | `fractional` = `numeric` let orderIndexAlias = `` // Scan the SELECT clause for ORDER_INDEX functions // TODO: Select is going to be optional in future - we will automatically add an // attribute for the index column for (const item of query.select!) { if (typeof item === `object`) { for (const [alias, expr] of Object.entries(item)) { if (typeof expr === `object` && isOrderIndexFunctionCall(expr)) { hasOrderIndexColumn = true orderIndexAlias = alias orderIndexType = getOrderIndexType(expr) break } } } if (hasOrderIndexColumn) break } // Normalize orderBy to an array of objects const orderByItems: OrderByItems = [] if (typeof query.orderBy === `string`) { // Handle string format: '@column' orderByItems.push({ operand: query.orderBy, direction: `asc`, }) } else if (Array.isArray(query.orderBy)) { // Handle array format: ['@column1', { '@column2': 'desc' }] for (const item of query.orderBy) { if (typeof item === `string`) { orderByItems.push({ operand: item, direction: `asc`, }) } else if (typeof item === `object`) { for (const [column, direction] of Object.entries(item)) { orderByItems.push({ operand: column, direction: direction as `asc` | `desc`, }) } } } } else if (typeof query.orderBy === `object`) { // Handle object format: { '@column': 'desc' } for (const [column, direction] of Object.entries(query.orderBy)) { orderByItems.push({ operand: column, direction: direction as `asc` | `desc`, }) } } // Create a value extractor function for the orderBy operator // const valueExtractor = ([key, namespacedRow]: [ const valueExtractor = (namespacedRow: NamespacedRow) => { // For multiple orderBy columns, create a composite key if (orderByItems.length > 1) { return orderByItems.map((item) => evaluateOperandOnNamespacedRow( namespacedRow, item.operand, mainTableAlias ) ) } else if (orderByItems.length === 1) { // For a single orderBy column, use the value directly const item = orderByItems[0] const val = evaluateOperandOnNamespacedRow( namespacedRow, item!.operand, mainTableAlias ) return val } // Default case - no ordering return null } const ascComparator = (a: any, b: any): number => { // if a and b are both strings, compare them based on locale if (typeof a === `string` && typeof b === `string`) { return a.localeCompare(b) } // if a and b are both arrays, compare them element by element if (Array.isArray(a) && Array.isArray(b)) { for (let i = 0; i < Math.min(a.length, b.length); i++) { // Compare the values const result = ascComparator(a[i], b[i]) if (result !== 0) { return result } } // All elements are equal up to the minimum length return a.length - b.length } // If at least one of the values is an object then we don't really know how to meaningfully compare them // therefore we turn them into strings and compare those // There are 2 exceptions: // 1) if both objects are dates then we can compare them // 2) if either object is nullish then we can't call toString on it const bothObjects = typeof a === `object` && typeof b === `object` const bothDates = a instanceof Date && b instanceof Date const notNull = a !== null && b !== null if (bothObjects && !bothDates && notNull) { // Every object should support `toString` return a.toString().localeCompare(b.toString()) } if (a < b) return -1 if (a > b) return 1 return 0 } const descComparator = (a: unknown, b: unknown): number => { return ascComparator(b, a) } // Create a multi-property comparator that respects the order and direction of each property const makeComparator = (orderByProps: OrderByItems) => { return (a: unknown, b: unknown) => { // If we're comparing arrays (multiple properties), compare each property in order if (orderByProps.length > 1) { // `a` and `b` must be arrays since `orderByItems.length > 1` // hence the extracted values must be arrays const arrayA = a as Array<unknown> const arrayB = b as Array<unknown> for (let i = 0; i < orderByProps.length; i++) { const direction = orderByProps[i]!.direction const compareFn = direction === `desc` ? descComparator : ascComparator const result = compareFn(arrayA[i], arrayB[i]) if (result !== 0) { return result } } // should normally always be 0 because // both values are extracted based on orderByItems return arrayA.length - arrayB.length } // Single property comparison if (orderByProps.length === 1) { const direction = orderByProps[0]!.direction return direction === `desc` ? descComparator(a, b) : ascComparator(a, b) } return ascComparator(a, b) } } const comparator = makeComparator(orderByItems) // Apply the appropriate orderBy operator based on whether an ORDER_INDEX column is requested if (hasOrderIndexColumn) { if (orderIndexType === `numeric`) { // Use orderByWithIndex for numeric indices resultPipeline = resultPipeline.pipe( orderByWithIndex(valueExtractor, { limit: query.limit, offset: query.offset, comparator, }), map(([key, [value, index]]) => { // Add the index to the result // We add this to the main table alias for now // TODO: re are going to need to refactor the whole order by pipeline const result = { ...(value as Record<string, unknown>), [mainTableAlias]: { ...value[mainTableAlias], [orderIndexAlias]: index, }, } return [key, result] as KeyedNamespacedRow }) ) } else { // Use orderByWithFractionalIndex for fractional indices resultPipeline = resultPipeline.pipe( orderByWithFractionalIndex(valueExtractor, { limit: query.limit, offset: query.offset, comparator, }), map(([key, [value, index]]) => { // Add the index to the result // We add this to the main table alias for now // TODO: re are going to need to refactor the whole order by pipeline const result = { ...(value as Record<string, unknown>), [mainTableAlias]: { ...value[mainTableAlias], [orderIndexAlias]: index, }, } return [key, result] as KeyedNamespacedRow }) ) } } else { // Use regular orderBy if no index column is requested resultPipeline = resultPipeline.pipe( orderBy(valueExtractor, { limit: query.limit, offset: query.offset, comparator, }) ) } return resultPipeline } // Helper function to extract the ORDER_INDEX type from a function call function getOrderIndexType(obj: any): `numeric` | `fractional` { if (!isOrderIndexFunctionCall(obj)) { throw new Error(`Not an ORDER_INDEX function call`) } const arg = obj[`ORDER_INDEX`] if (arg === `numeric` || arg === true || arg === `default`) { return `numeric` } else if (arg === `fractional`) { return `fractional` } else { throw new Error(`Invalid ORDER_INDEX type: ` + arg) } }