{"version":3,"file":"index-optimization.cjs","sources":["../../../src/utils/index-optimization.ts"],"sourcesContent":["/**\n * # Index-Based Query Optimization\n *\n * This module provides utilities for optimizing query expressions by leveraging\n * available indexes to quickly find matching keys instead of scanning all data.\n *\n * This is different from the query structure optimizer in `query/optimizer.ts`\n * which rewrites query IR structure. This module focuses on using indexes during\n * query execution to speed up data filtering.\n *\n * ## Key Features:\n * - Uses indexes to find matching keys for WHERE conditions\n * - Supports AND/OR logic with set operations\n * - Handles range queries (eq, gt, gte, lt, lte)\n * - Optimizes IN array expressions\n */\n\nimport { DEFAULT_COMPARE_OPTIONS } from '../utils.js'\nimport { ReverseIndex } from '../indexes/reverse-index.js'\nimport { hasVirtualPropPath } from '../virtual-props.js'\nimport { makeComparator } from './comparison.js'\nimport type { CompareOptions } from '../query/builder/types.js'\nimport type { IndexInterface, IndexOperation } from '../indexes/base-index.js'\nimport type { BasicExpression } from '../query/ir.js'\nimport type { CollectionLike } from '../types.js'\n\n/**\n * Result of index-based query optimization\n */\nexport interface OptimizationResult<TKey> {\n  canOptimize: boolean\n  matchingKeys: Set<TKey>\n  /**\n   * Whether `matchingKeys` is exactly the set of keys matching the expression.\n   * When `false`, the keys are a superset of the true result (some conditions\n   * could not be served by an index) and each row must be re-checked against\n   * the full expression before being included in the result.\n   */\n  isExact: boolean\n}\n\n/**\n * Finds an index that matches a given field path\n */\nexport function findIndexForField<TKey extends string | number>(\n  collection: CollectionLike<any, TKey>,\n  fieldPath: Array<string>,\n  compareOptions?: CompareOptions,\n): IndexInterface<TKey> | undefined {\n  if (hasVirtualPropPath(fieldPath)) {\n    return undefined\n  }\n  const compareOpts = compareOptions ?? {\n    ...DEFAULT_COMPARE_OPTIONS,\n    ...collection.compareOptions,\n  }\n\n  for (const index of collection.indexes.values()) {\n    if (\n      index.matchesField(fieldPath) &&\n      index.matchesCompareOptions(compareOpts)\n    ) {\n      if (!index.matchesDirection(compareOpts.direction)) {\n        return new ReverseIndex(index)\n      }\n      return index\n    }\n  }\n  return undefined\n}\n\n/**\n * Intersects multiple sets (AND logic)\n */\nexport function intersectSets<T>(sets: Array<Set<T>>): Set<T> {\n  if (sets.length === 0) return new Set()\n  if (sets.length === 1) return new Set(sets[0])\n\n  let result = new Set(sets[0])\n  for (let i = 1; i < sets.length; i++) {\n    const newResult = new Set<T>()\n    for (const item of result) {\n      if (sets[i]!.has(item)) {\n        newResult.add(item)\n      }\n    }\n    result = newResult\n  }\n  return result\n}\n\n/**\n * Unions multiple sets (OR logic)\n */\nexport function unionSets<T>(sets: Array<Set<T>>): Set<T> {\n  const result = new Set<T>()\n  for (const set of sets) {\n    for (const item of set) {\n      result.add(item)\n    }\n  }\n  return result\n}\n\n/**\n * Whether a value can be matched exactly by an index lookup, i.e. the index\n * result for it is not a superset that the caller must re-filter.\n *\n * Only `null`/`undefined` are inexact: the WHERE evaluator's three-valued logic\n * makes any comparison against them UNKNOWN, yet a BTree index stores and\n * returns rows with nullish keys (they sort to the nulls end), so a result that\n * could include such rows must be re-filtered.\n *\n * `NaN` and invalid Dates are exact: under the engine's PostgreSQL float\n * semantics they are equal to themselves and ordered (greatest non-null value),\n * so the evaluator and the index agree on them and no re-filtering is needed.\n */\nfunction isExactComparisonValue(value: unknown): boolean {\n  return value != null\n}\n\n/**\n * Whether the collection orders strings using locale collation.\n *\n * Under `stringSort: 'locale'` a BTree string index orders values with\n * `localeCompare`, but the WHERE evaluator compares strings with JS relational\n * operators (code-point order). For range predicates these orders disagree\n * (e.g. `'ö' > 'z'` is true in JS but `'ö'` sorts before `'z'` under locale\n * `en`), so an index range lookup can omit matching rows. Such omissions cannot\n * be recovered by re-filtering, so locale-backed string range predicates must\n * not be index-optimized.\n */\nfunction usesLocaleStringSort(collection: CollectionLike<any, any>): boolean {\n  const opts = { ...DEFAULT_COMPARE_OPTIONS, ...collection.compareOptions }\n  return opts.stringSort === `locale`\n}\n\n/**\n * Whether a range predicate on this operand would use an index ordering that\n * differs from the WHERE evaluator's relational operators, so an index range\n * lookup could omit genuine matches that re-filtering cannot recover.\n *\n * The evaluator compares with JS relational operators (extended with\n * PostgreSQL float semantics for `NaN`/invalid Dates). That order matches the\n * index comparator for numbers, booleans, bigints, lexically-sorted strings,\n * Dates (valid, ordered by time; invalid, ordered as the greatest value) and\n * `NaN`. It diverges for locale-sorted strings (localeCompare vs code-point\n * order) and for arrays, plain objects, Temporal values and typed arrays\n * (recursive/identity ordering vs string coercion).\n *\n * Note: `null`/`undefined` operands are not handled here — those are superset\n * cases handled by re-filtering ({@link isExactComparisonValue}).\n */\nfunction isRangeOrderingDivergent(\n  value: unknown,\n  collection: CollectionLike<any, any>,\n): boolean {\n  switch (typeof value) {\n    case `number`:\n    case `bigint`:\n    case `boolean`:\n      return false\n    case `string`:\n      return usesLocaleStringSort(collection)\n    case `object`: {\n      if (value === null) return false\n      // Dates order consistently with the evaluator: valid Dates by time, and\n      // invalid Dates as the greatest value under PostgreSQL float semantics.\n      return !(value instanceof Date)\n    }\n    default:\n      return false\n  }\n}\n\n/**\n * Whether a range predicate (gt/gte/lt/lte) on this operand can be safely\n * served by the given index: the operand's domain must order the same way the\n * index does, and the index itself must support trustworthy range traversal\n * (no custom comparator).\n */\nfunction canRangeOptimize(\n  value: unknown,\n  index: IndexInterface<any>,\n  collection: CollectionLike<any, any>,\n): boolean {\n  return (\n    !isRangeOrderingDivergent(value, collection) &&\n    index.supportsRangeOptimization\n  )\n}\n\n/**\n * Optimizes a query expression using available indexes to find matching keys\n */\nexport function optimizeExpressionWithIndexes<\n  T extends object,\n  TKey extends string | number,\n>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): OptimizationResult<TKey> {\n  return optimizeQueryRecursive(expression, collection)\n}\n\n/**\n * Recursively optimizes query expressions\n */\nfunction optimizeQueryRecursive<T extends object, TKey extends string | number>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): OptimizationResult<TKey> {\n  if (expression.type === `func`) {\n    switch (expression.name) {\n      case `eq`:\n      case `gt`:\n      case `gte`:\n      case `lt`:\n      case `lte`:\n        return optimizeSimpleComparison(expression, collection)\n\n      case `and`:\n        return optimizeAndExpression(expression, collection)\n\n      case `or`:\n        return optimizeOrExpression(expression, collection)\n\n      case `in`:\n        return optimizeInArrayExpression(expression, collection)\n    }\n  }\n\n  return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n}\n\n/**\n * Checks if an expression can be optimized\n */\nexport function canOptimizeExpression<\n  T extends object,\n  TKey extends string | number,\n>(expression: BasicExpression, collection: CollectionLike<T, TKey>): boolean {\n  if (expression.type === `func`) {\n    switch (expression.name) {\n      case `eq`:\n      case `gt`:\n      case `gte`:\n      case `lt`:\n      case `lte`:\n        return canOptimizeSimpleComparison(expression, collection)\n\n      case `and`:\n        return canOptimizeAndExpression(expression, collection)\n\n      case `or`:\n        return canOptimizeOrExpression(expression, collection)\n\n      case `in`:\n        return canOptimizeInArrayExpression(expression, collection)\n    }\n  }\n\n  return false\n}\n\n/**\n * Result of compound range optimization, including which AND arguments\n * were covered by the range query so the caller can process the rest.\n */\ninterface CompoundRangeResult<TKey> extends OptimizationResult<TKey> {\n  coveredArgIndices: Set<number>\n}\n\n/**\n * Optimizes compound range queries on the same field\n * Example: WHERE age > 5 AND age < 10\n */\nfunction optimizeCompoundRangeQuery<\n  T extends object,\n  TKey extends string | number,\n>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): CompoundRangeResult<TKey> {\n  if (expression.type !== `func` || expression.args.length < 2) {\n    return {\n      canOptimize: false,\n      matchingKeys: new Set(),\n      isExact: false,\n      coveredArgIndices: new Set(),\n    }\n  }\n\n  // Group range operations by field\n  const fieldOperations = new Map<\n    string,\n    Array<{\n      operation: `gt` | `gte` | `lt` | `lte`\n      value: any\n      argIndex: number\n    }>\n  >()\n\n  // Collect all range operations from AND arguments\n  for (const [argIndex, arg] of expression.args.entries()) {\n    if (arg.type === `func` && [`gt`, `gte`, `lt`, `lte`].includes(arg.name)) {\n      const rangeOp = arg as any\n      if (rangeOp.args.length === 2) {\n        const leftArg = rangeOp.args[0]!\n        const rightArg = rangeOp.args[1]!\n\n        // Check both directions: field op value AND value op field\n        let fieldArg: BasicExpression | null = null\n        let valueArg: BasicExpression | null = null\n        let operation = rangeOp.name as `gt` | `gte` | `lt` | `lte`\n\n        if (leftArg.type === `ref` && rightArg.type === `val`) {\n          // field op value\n          fieldArg = leftArg\n          valueArg = rightArg\n        } else if (leftArg.type === `val` && rightArg.type === `ref`) {\n          // value op field - need to flip the operation\n          fieldArg = rightArg\n          valueArg = leftArg\n\n          // Flip the operation for reverse comparison\n          switch (operation) {\n            case `gt`:\n              operation = `lt`\n              break\n            case `gte`:\n              operation = `lte`\n              break\n            case `lt`:\n              operation = `gt`\n              break\n            case `lte`:\n              operation = `gte`\n              break\n          }\n        }\n\n        if (fieldArg && valueArg) {\n          const fieldPath = (fieldArg as any).path\n          const fieldKey = fieldPath.join(`.`)\n          const value = (valueArg as any).value\n\n          if (!fieldOperations.has(fieldKey)) {\n            fieldOperations.set(fieldKey, [])\n          }\n          fieldOperations.get(fieldKey)!.push({ operation, value, argIndex })\n        }\n      }\n    }\n  }\n\n  // Check if we have multiple operations on the same field\n  for (const [fieldKey, operations] of fieldOperations) {\n    if (operations.length >= 2) {\n      const fieldPath = fieldKey.split(`.`)\n      const index = findIndexForField(collection, fieldPath)\n\n      // Only collapse this field into a range query when every bound's domain\n      // orders the same way the index does and the index supports trustworthy\n      // range traversal. Otherwise the index may omit matching rows that\n      // re-filtering cannot recover, so leave the field for a full scan.\n      if (\n        index &&\n        operations.some((op) => !canRangeOptimize(op.value, index, collection))\n      ) {\n        continue\n      }\n\n      if (index && index.supports(`gt`) && index.supports(`lt`)) {\n        // Compare values with the same semantics the index uses (dates,\n        // locale strings, ...), in ascending order since bounds are about\n        // value order regardless of the index direction\n        const compare = makeComparator({\n          ...DEFAULT_COMPARE_OPTIONS,\n          ...collection.compareOptions,\n          direction: `asc`,\n        })\n\n        // Build range query options, keeping the strictest bound on each\n        // side: a larger lower bound (or smaller upper bound) wins, and at\n        // equal values the exclusive operation wins over the inclusive one.\n        // `hasFromBound`/`hasToBound` track whether a bound was selected,\n        // separately from the bound value (which may legitimately be falsy).\n        let from: any = undefined\n        let to: any = undefined\n        let hasFromBound = false\n        let hasToBound = false\n        let fromInclusive = true\n        let toInclusive = true\n        // A comparison against null/undefined is never true, but in an index\n        // nullish values sort to the nulls end, so a range query cannot\n        // represent such a bound. Track it and force a re-filter instead of\n        // claiming the result is exact. (NaN/invalid Dates are ordered and\n        // comparable under PostgreSQL semantics, so they are real bounds.)\n        let hasNonComparableBound = false\n\n        for (const { operation, value } of operations) {\n          if (!isExactComparisonValue(value)) {\n            hasNonComparableBound = true\n            continue\n          }\n          switch (operation) {\n            case `gt`:\n            case `gte`: {\n              const cmp = hasFromBound ? compare(value, from) : 1\n              if (cmp > 0) {\n                from = value\n                hasFromBound = true\n                fromInclusive = operation === `gte`\n              } else if (cmp === 0 && operation === `gt`) {\n                fromInclusive = false\n              }\n              break\n            }\n            case `lt`:\n            case `lte`: {\n              const cmp = hasToBound ? compare(value, to) : -1\n              if (cmp < 0) {\n                to = value\n                hasToBound = true\n                toInclusive = operation === `lte`\n              } else if (cmp === 0 && operation === `lt`) {\n                toInclusive = false\n              }\n              break\n            }\n          }\n        }\n\n        // Only pass the bounds that were selected: rangeQuery distinguishes\n        // an absent bound (open-ended) from an explicitly provided one\n        const rangeOptions: Record<string, any> = {}\n        if (hasFromBound) {\n          rangeOptions.from = from\n          rangeOptions.fromInclusive = fromInclusive\n        }\n        if (hasToBound) {\n          rangeOptions.to = to\n          rangeOptions.toInclusive = toInclusive\n        }\n        const matchingKeys = (index as any).rangeQuery(rangeOptions)\n\n        return {\n          canOptimize: true,\n          matchingKeys,\n          // The range result is exact only when it cannot include rows with a\n          // nullish indexed value (which a comparison would reject but the\n          // index returns, as they sort as the smallest key). That requires a\n          // non-nullish lower bound to exclude them: without `hasFromBound`\n          // the range is open at the bottom and captures those rows, and a\n          // non-comparable bound value (`hasNonComparableBound`) can never\n          // bound them out.\n          isExact: hasFromBound && !hasNonComparableBound,\n          coveredArgIndices: new Set(operations.map((op) => op.argIndex)),\n        }\n      }\n    }\n  }\n\n  return {\n    canOptimize: false,\n    matchingKeys: new Set(),\n    isExact: false,\n    coveredArgIndices: new Set(),\n  }\n}\n\n/**\n * Optimizes simple comparison expressions (eq, gt, gte, lt, lte)\n */\nfunction optimizeSimpleComparison<\n  T extends object,\n  TKey extends string | number,\n>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): OptimizationResult<TKey> {\n  if (expression.type !== `func` || expression.args.length !== 2) {\n    return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n  }\n\n  const leftArg = expression.args[0]!\n  const rightArg = expression.args[1]!\n\n  // Check both directions: field op value AND value op field\n  let fieldArg: BasicExpression | null = null\n  let valueArg: BasicExpression | null = null\n  let operation = expression.name as `eq` | `gt` | `gte` | `lt` | `lte`\n\n  if (leftArg.type === `ref` && rightArg.type === `val`) {\n    // field op value\n    fieldArg = leftArg\n    valueArg = rightArg\n  } else if (leftArg.type === `val` && rightArg.type === `ref`) {\n    // value op field - need to flip the operation\n    fieldArg = rightArg\n    valueArg = leftArg\n\n    // Flip the operation for reverse comparison\n    switch (operation) {\n      case `gt`:\n        operation = `lt`\n        break\n      case `gte`:\n        operation = `lte`\n        break\n      case `lt`:\n        operation = `gt`\n        break\n      case `lte`:\n        operation = `gte`\n        break\n      // eq stays the same\n    }\n  }\n\n  if (fieldArg && valueArg) {\n    const fieldPath = (fieldArg as any).path\n    const index = findIndexForField(collection, fieldPath)\n\n    if (index) {\n      const queryValue = (valueArg as any).value\n\n      // Map operation to IndexOperation enum\n      const indexOperation = operation as IndexOperation\n\n      // Check if the index supports this operation\n      if (!index.supports(indexOperation)) {\n        return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n      }\n\n      // A range op can only use the index when the operand's domain orders the\n      // same way the index does and the index supports trustworthy traversal.\n      // Otherwise the index may omit matching rows, which re-filtering cannot\n      // recover, so fall back to a full scan.\n      if (\n        (operation === `gt` ||\n          operation === `gte` ||\n          operation === `lt` ||\n          operation === `lte`) &&\n        !canRangeOptimize(queryValue, index, collection)\n      ) {\n        return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n      }\n\n      const matchingKeys = index.lookup(indexOperation, queryValue)\n\n      // A comparison against a nullish value is never true, but BTree indexes\n      // store and return rows with nullish keys (they sort to the nulls end).\n      // Determine whether the index result is exact or a superset that the\n      // caller must re-filter:\n      // - eq/gt/gte: a nullish query value matches nothing while the index\n      //   still returns nullish-keyed rows -> inexact. A non-nullish lower\n      //   bound (gt/gte) excludes those bottom-sorted rows, so they stay exact.\n      // - lt/lte: the open lower bound always includes nullish-keyed rows,\n      //   so the result is conservatively inexact.\n      // NaN/invalid Dates are exact here: under PostgreSQL float semantics the\n      // evaluator and the index agree on them (equal to self, greatest).\n      const isExact =\n        operation === `lt` || operation === `lte`\n          ? false\n          : isExactComparisonValue(queryValue)\n\n      return { canOptimize: true, matchingKeys, isExact }\n    }\n  }\n\n  return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n}\n\n/**\n * Checks if a simple comparison can be optimized\n */\nfunction canOptimizeSimpleComparison<\n  T extends object,\n  TKey extends string | number,\n>(expression: BasicExpression, collection: CollectionLike<T, TKey>): boolean {\n  if (expression.type !== `func` || expression.args.length !== 2) {\n    return false\n  }\n\n  const leftArg = expression.args[0]!\n  const rightArg = expression.args[1]!\n\n  // Check both directions: field op value AND value op field\n  let fieldPath: Array<string> | null = null\n\n  if (leftArg.type === `ref` && rightArg.type === `val`) {\n    fieldPath = (leftArg as any).path\n  } else if (leftArg.type === `val` && rightArg.type === `ref`) {\n    fieldPath = (rightArg as any).path\n  }\n\n  if (fieldPath) {\n    const index = findIndexForField(collection, fieldPath)\n    return index !== undefined\n  }\n\n  return false\n}\n\n/**\n * Optimizes AND expressions\n */\nfunction optimizeAndExpression<T extends object, TKey extends string | number>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): OptimizationResult<TKey> {\n  if (expression.type !== `func` || expression.args.length < 2) {\n    return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n  }\n\n  // First, try to optimize compound range queries on the same field\n  // (e.g. age > 5 AND age < 10 becomes a single range query)\n  const compoundRangeResult = optimizeCompoundRangeQuery(expression, collection)\n  const coveredArgIndices = compoundRangeResult.canOptimize\n    ? compoundRangeResult.coveredArgIndices\n    : new Set<number>()\n\n  const results: Array<OptimizationResult<TKey>> = []\n  if (compoundRangeResult.canOptimize) {\n    results.push(compoundRangeResult)\n  }\n\n  // Try to optimize the remaining conjuncts, keep the optimizable ones.\n  // Conjuncts that cannot use an index make the result inexact: the\n  // intersection is then a superset of the true result and must be\n  // re-filtered against the full expression by the caller. The compound\n  // range result may itself be inexact (e.g. a null/undefined bound).\n  let allConjunctsExact = !compoundRangeResult.canOptimize\n    ? true\n    : compoundRangeResult.isExact\n  for (const [argIndex, arg] of expression.args.entries()) {\n    if (coveredArgIndices.has(argIndex)) {\n      continue\n    }\n    const result = optimizeQueryRecursive(arg, collection)\n    if (result.canOptimize) {\n      results.push(result)\n      if (!result.isExact) {\n        allConjunctsExact = false\n      }\n    } else {\n      allConjunctsExact = false\n    }\n  }\n\n  if (results.length > 0) {\n    // Use intersectSets utility for AND logic\n    const allMatchingSets = results.map((r) => r.matchingKeys)\n    const intersectedKeys = intersectSets(allMatchingSets)\n    return {\n      canOptimize: true,\n      matchingKeys: intersectedKeys,\n      isExact: allConjunctsExact,\n    }\n  }\n\n  return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n}\n\n/**\n * Checks if an AND expression can be optimized\n */\nfunction canOptimizeAndExpression<\n  T extends object,\n  TKey extends string | number,\n>(expression: BasicExpression, collection: CollectionLike<T, TKey>): boolean {\n  if (expression.type !== `func` || expression.args.length < 2) {\n    return false\n  }\n\n  // If any argument can be optimized, we can gain some speedup\n  return expression.args.some((arg) => canOptimizeExpression(arg, collection))\n}\n\n/**\n * Optimizes OR expressions\n */\nfunction optimizeOrExpression<T extends object, TKey extends string | number>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): OptimizationResult<TKey> {\n  if (expression.type !== `func` || expression.args.length < 2) {\n    return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n  }\n\n  const results: Array<OptimizationResult<TKey>> = []\n\n  // Every disjunct must be optimizable: rows matched only by a disjunct\n  // that cannot use an index would be missing from the union, and no\n  // post-filtering can recover them. In that case fall back to a full scan.\n  for (const arg of expression.args) {\n    const result = optimizeQueryRecursive(arg, collection)\n    if (!result.canOptimize) {\n      return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n    }\n    results.push(result)\n  }\n\n  // Use unionSets utility for OR logic\n  const allMatchingSets = results.map((r) => r.matchingKeys)\n  const unionedKeys = unionSets(allMatchingSets)\n  return {\n    canOptimize: true,\n    matchingKeys: unionedKeys,\n    // An inexact (superset) disjunct makes the union a superset as well\n    isExact: results.every((r) => r.isExact),\n  }\n}\n\n/**\n * Checks if an OR expression can be optimized\n */\nfunction canOptimizeOrExpression<\n  T extends object,\n  TKey extends string | number,\n>(expression: BasicExpression, collection: CollectionLike<T, TKey>): boolean {\n  if (expression.type !== `func` || expression.args.length < 2) {\n    return false\n  }\n\n  // Every disjunct must be optimizable, otherwise the union would miss\n  // rows matched only by the non-optimizable disjuncts\n  return expression.args.every((arg) => canOptimizeExpression(arg, collection))\n}\n\n/**\n * Optimizes IN array expressions\n */\nfunction optimizeInArrayExpression<\n  T extends object,\n  TKey extends string | number,\n>(\n  expression: BasicExpression,\n  collection: CollectionLike<T, TKey>,\n): OptimizationResult<TKey> {\n  if (expression.type !== `func` || expression.args.length !== 2) {\n    return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n  }\n\n  const fieldArg = expression.args[0]!\n  const arrayArg = expression.args[1]!\n\n  if (\n    fieldArg.type === `ref` &&\n    arrayArg.type === `val` &&\n    Array.isArray((arrayArg as any).value)\n  ) {\n    const fieldPath = (fieldArg as any).path\n    const values = (arrayArg as any).value\n    const index = findIndexForField(collection, fieldPath)\n\n    // A nullish or NaN member can never be matched by `IN` (a comparison\n    // against null/undefined/NaN is never true), but the index would still\n    // return rows with such an indexed value. When the list contains one of\n    // those the result is a superset that the caller must re-filter.\n    const isExact = values.every((value: any) => isExactComparisonValue(value))\n\n    if (index) {\n      // Check if the index supports IN operation\n      if (index.supports(`in`)) {\n        const matchingKeys = index.lookup(`in`, values)\n        return { canOptimize: true, matchingKeys, isExact }\n      } else if (index.supports(`eq`)) {\n        // Fallback to multiple equality lookups\n        const matchingKeys = new Set<TKey>()\n        for (const value of values) {\n          const keysForValue = index.lookup(`eq`, value)\n          for (const key of keysForValue) {\n            matchingKeys.add(key)\n          }\n        }\n        return { canOptimize: true, matchingKeys, isExact }\n      }\n    }\n  }\n\n  return { canOptimize: false, matchingKeys: new Set(), isExact: false }\n}\n\n/**\n * Checks if an IN array expression can be optimized\n */\nfunction canOptimizeInArrayExpression<\n  T extends object,\n  TKey extends string | number,\n>(expression: BasicExpression, collection: CollectionLike<T, TKey>): boolean {\n  if (expression.type !== `func` || expression.args.length !== 2) {\n    return false\n  }\n\n  const fieldArg = expression.args[0]!\n  const arrayArg = expression.args[1]!\n\n  if (\n    fieldArg.type === `ref` &&\n    arrayArg.type === `val` &&\n    Array.isArray((arrayArg as any).value)\n  ) {\n    const fieldPath = (fieldArg as any).path\n    const index = findIndexForField(collection, fieldPath)\n    return index !== undefined\n  }\n\n  return false\n}\n"],"names":["hasVirtualPropPath","DEFAULT_COMPARE_OPTIONS","ReverseIndex","makeComparator"],"mappings":"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"}