UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1 lines 35.5 kB
{"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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"}