{"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 { PropRef, followRef } from '../ir.js'\nimport { ensureIndexForField } from '../../indexes/auto-index.js'\nimport { findIndexForField } from '../../utils/index-optimization.js'\nimport { compileExpression } from './evaluators.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 { IndexInterface } from '../../indexes/base-index.js'\nimport type { Collection } from '../../collection/index.js'\n\nexport type OrderByOptimizationInfo = {\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  /** Extracts only the first column value - used for index-based cursor */\n  firstColumnValueExtractor: (row: Record<string, unknown>) => unknown\n  /** Index on the first orderBy column - used for lazy loading */\n  index?: IndexInterface<string | number>\n  dataNeeded?: () => number\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\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 (limit && !groupKeyFn) {\n    let index: IndexInterface<string | number> | undefined\n    let followRefCollection: Collection | undefined\n    let firstColumnValueExtractor: CompiledSingleRowExpression | undefined\n    let orderByAlias: string = rawQuery.from.alias\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    if (firstOrderByExpression.type === `ref`) {\n      const followRefResult = followRef(\n        rawQuery,\n        firstOrderByExpression,\n        collection,\n      )\n\n      if (followRefResult) {\n        followRefCollection = followRefResult.collection\n        const fieldName = followRefResult.path[0]\n        const compareOpts = buildCompareOptions(\n          firstClause,\n          followRefCollection,\n        )\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        // First column value extractor - used for index cursor\n        firstColumnValueExtractor = compileExpression(\n          new PropRef(followRefResult.path),\n          true,\n        ) as CompiledSingleRowExpression\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      }\n    }\n\n    // Only create comparator and value extractors if the first column is a ref expression\n    // For aggregate or computed expressions, we can't extract values from raw collection rows\n    if (!firstColumnValueExtractor) {\n      // Skip optimization for non-ref expressions (aggregates, computed values, etc.)\n      // The query will still work, but without lazy loading optimization\n    } else {\n      // Build value extractors for all columns (must all be ref expressions for multi-column)\n      // Check if all orderBy expressions are ref types (required for multi-column extraction)\n      const allColumnsAreRefs = orderByClause.every(\n        (clause) => clause.expression.type === `ref`,\n      )\n\n      // Create extractors for all columns if they're all refs\n      const allColumnExtractors:\n        | Array<CompiledSingleRowExpression>\n        | undefined = allColumnsAreRefs\n        ? orderByClause.map((clause) => {\n            // We know it's a ref since we checked allColumnsAreRefs\n            const refExpr = clause.expression as PropRef\n            const followResult = followRef(rawQuery, refExpr, collection)\n            if (followResult) {\n              return compileExpression(\n                new PropRef(followResult.path),\n                true,\n              ) as CompiledSingleRowExpression\n            }\n            // Fallback for refs that don't follow\n            return compileExpression(\n              clause.expression,\n              true,\n            ) as CompiledSingleRowExpression\n          })\n        : undefined\n\n      // Create a comparator for raw rows (used for tracking sent values)\n      // This compares ALL orderBy columns for proper ordering\n      const comparator = (\n        a: Record<string, unknown> | null | undefined,\n        b: Record<string, unknown> | null | undefined,\n      ) => {\n        if (orderByClause.length === 1) {\n          // Single column: extract and compare\n          const extractedA = a ? firstColumnValueExtractor(a) : a\n          const extractedB = b ? firstColumnValueExtractor(b) : b\n          return compare(extractedA, extractedB)\n        }\n        if (allColumnExtractors) {\n          // Multi-column with all refs: extract all values and compare\n          const extractAll = (\n            row: Record<string, unknown> | null | undefined,\n          ) => {\n            if (!row) return row\n            return allColumnExtractors.map((extractor) => extractor(row))\n          }\n          return compare(extractAll(a), extractAll(b))\n        }\n        // Fallback: can't compare (shouldn't happen since we skip non-ref cases)\n        return 0\n      }\n\n      // Create a value extractor for raw rows that extracts ALL orderBy column values\n      // This is used for tracking sent values and building composite cursors\n      const rawRowValueExtractor = (row: Record<string, unknown>): unknown => {\n        if (orderByClause.length === 1) {\n          // Single column: return single value\n          return firstColumnValueExtractor(row)\n        }\n        if (allColumnExtractors) {\n          // Multi-column: return array of all values\n          return allColumnExtractors.map((extractor) => extractor(row))\n        }\n        // Fallback (shouldn't happen)\n        return undefined\n      }\n\n      orderByOptimizationInfo = {\n        alias: orderByAlias,\n        offset: offset ?? 0,\n        limit,\n        comparator,\n        valueExtractorForRawRow: rawRowValueExtractor,\n        firstColumnValueExtractor: firstColumnValueExtractor,\n        index,\n        orderBy: orderByClause,\n      }\n\n      // Store the optimization info keyed by collection ID\n      // Use the followed collection if available, otherwise use the main collection\n      const targetCollectionId = followRefCollection?.id ?? collection.id\n      optimizableOrderByCollections[targetCollectionId] =\n        orderByOptimizationInfo\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[targetCollectionId]![`dataNeeded`] =\n            () => {\n              const size = getSize()\n              return Math.max(0, orderByOptimizationInfo!.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 */\nexport function buildCompareOptions(\n  clause: OrderByClause,\n  collection: CollectionLike<any, any>,\n): CompareOptions {\n  if (clause.compareOptions.stringSort !== undefined) {\n    return clause.compareOptions\n  }\n\n  return {\n    ...collection.compareOptions,\n    direction: clause.compareOptions.direction,\n    nulls: clause.compareOptions.nulls,\n  }\n}\n"],"names":["replaceAggregatesByRefs","compileExpression","makeComparator","defaultComparator","followRef","ensureIndexForField","PropRef","findIndexForField","groupedOrderByWithFractionalIndex","orderByWithFractionalIndex"],"mappings":"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"}