UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1,515 lines (1,358 loc) 45.9 kB
import { Func, Value } from './ir.js' import type { BasicExpression, OrderBy, PropRef } from './ir.js' import type { LoadSubsetOptions } from '../types.js' /** * Check if one where clause is a logical subset of another. * Returns true if the subset predicate is more restrictive than (or equal to) the superset predicate. * * @example * // age > 20 is subset of age > 10 (more restrictive) * isWhereSubset(gt(ref('age'), val(20)), gt(ref('age'), val(10))) // true * * @example * // age > 10 AND name = 'X' is subset of age > 10 (more conditions) * isWhereSubset(and(gt(ref('age'), val(10)), eq(ref('name'), val('X'))), gt(ref('age'), val(10))) // true * * @param subset - The potentially more restrictive predicate * @param superset - The potentially less restrictive predicate * @returns true if subset logically implies superset */ export function isWhereSubset( subset: BasicExpression<boolean> | undefined, superset: BasicExpression<boolean> | undefined, ): boolean { // undefined/missing where clause means "no filter" (all data) // Both undefined means subset relationship holds (all data ⊆ all data) if (subset === undefined && superset === undefined) { return true } // If subset is undefined but superset is not, we're requesting ALL data // but have only loaded SOME data - subset relationship does NOT hold if (subset === undefined && superset !== undefined) { return false } // If superset is undefined (no filter = all data loaded), // then any constrained subset is contained if (superset === undefined && subset !== undefined) { return true } return isWhereSubsetInternal(subset!, superset!) } function makeDisjunction( preds: Array<BasicExpression<boolean>>, ): BasicExpression<boolean> { if (preds.length === 0) { return new Value(false) } if (preds.length === 1) { return preds[0]! } return new Func(`or`, preds) } function convertInToOr(inField: InField) { const equalities = inField.values.map( (value) => new Func(`eq`, [inField.ref, new Value(value)]), ) return makeDisjunction(equalities) } function isWhereSubsetInternal( subset: BasicExpression<boolean>, superset: BasicExpression<boolean>, ): boolean { // If subset is false it is requesting no data, // thus the result set is empty // and the empty set is a subset of any set if (subset.type === `val` && subset.value === false) { return true } // If expressions are structurally equal, subset relationship holds if (areExpressionsEqual(subset, superset)) { return true } // Handle superset being an AND: subset must imply ALL conjuncts // If superset is (A AND B), then subset ⊆ (A AND B) only if subset ⊆ A AND subset ⊆ B // Example: (age > 20) ⊆ (age > 10 AND status = 'active') is false (doesn't imply status condition) if (superset.type === `func` && superset.name === `and`) { return superset.args.every((arg) => isWhereSubsetInternal(subset, arg as BasicExpression<boolean>), ) } // Handle subset being an AND: (A AND B) implies both A and B if (subset.type === `func` && subset.name === `and`) { // For (A AND B) ⊆ C, since (A AND B) implies A, we check if any conjunct implies C return subset.args.some((arg) => isWhereSubsetInternal(arg as BasicExpression<boolean>, superset), ) } // Turn x IN [A, B, C] into x = A OR x = B OR x = C // for unified handling of IN and OR if (subset.type === `func` && subset.name === `in`) { const inField = extractInField(subset) if (inField) { return isWhereSubsetInternal(convertInToOr(inField), superset) } } if (superset.type === `func` && superset.name === `in`) { const inField = extractInField(superset) if (inField) { return isWhereSubsetInternal(subset, convertInToOr(inField)) } } // Handle OR in subset: (A OR B) is subset of C only if both A and B are subsets of C if (subset.type === `func` && subset.name === `or`) { return subset.args.every((arg) => isWhereSubsetInternal(arg as BasicExpression<boolean>, superset), ) } // Handle OR in superset: subset ⊆ (A OR B) if subset ⊆ A or subset ⊆ B // (A OR B) as superset means data can satisfy A or B // If subset is contained in any disjunct, it's contained in the union if (superset.type === `func` && superset.name === `or`) { return superset.args.some((arg) => isWhereSubsetInternal(subset, arg as BasicExpression<boolean>), ) } // Handle comparison operators on the same field if (subset.type === `func` && superset.type === `func`) { const subsetFunc = subset as Func const supersetFunc = superset as Func // Check if both are comparisons on the same field const subsetField = extractComparisonField(subsetFunc) const supersetField = extractComparisonField(supersetFunc) if ( subsetField && supersetField && areRefsEqual(subsetField.ref, supersetField.ref) ) { return isComparisonSubset( subsetFunc, subsetField.value, supersetFunc, supersetField.value, ) } /* // Handle eq vs in if (subsetFunc.name === `eq` && supersetFunc.name === `in`) { const subsetFieldEq = extractEqualityField(subsetFunc) const supersetFieldIn = extractInField(supersetFunc) if ( subsetFieldEq && supersetFieldIn && areRefsEqual(subsetFieldEq.ref, supersetFieldIn.ref) ) { // field = X is subset of field IN [X, Y, Z] if X is in the array // Use cached primitive set and metadata from extraction return arrayIncludesWithSet( supersetFieldIn.values, subsetFieldEq.value, supersetFieldIn.primitiveSet ?? null, supersetFieldIn.areAllPrimitives ) } } // Handle in vs in if (subsetFunc.name === `in` && supersetFunc.name === `in`) { const subsetFieldIn = extractInField(subsetFunc) const supersetFieldIn = extractInField(supersetFunc) if ( subsetFieldIn && supersetFieldIn && areRefsEqual(subsetFieldIn.ref, supersetFieldIn.ref) ) { // field IN [A, B] is subset of field IN [A, B, C] if all values in subset are in superset // Use cached primitive set and metadata from extraction return subsetFieldIn.values.every((subVal) => arrayIncludesWithSet( supersetFieldIn.values, subVal, supersetFieldIn.primitiveSet ?? null, supersetFieldIn.areAllPrimitives ) ) } } */ } // Conservative: if we can't determine, return false return false } /** * Helper to combine where predicates with common logic for AND/OR operations */ function combineWherePredicates( predicates: Array<BasicExpression<boolean>>, operation: `and` | `or`, simplifyFn: ( preds: Array<BasicExpression<boolean>>, ) => BasicExpression<boolean> | null, ): BasicExpression<boolean> { const emptyValue = operation === `and` ? true : false const identityValue = operation === `and` ? true : false if (predicates.length === 0) { return { type: `val`, value: emptyValue } as BasicExpression<boolean> } if (predicates.length === 1) { return predicates[0]! } // Flatten nested expressions of the same operation const flatPredicates: Array<BasicExpression<boolean>> = [] for (const pred of predicates) { if (pred.type === `func` && pred.name === operation) { flatPredicates.push(...pred.args) } else { flatPredicates.push(pred) } } // Group predicates by field for simplification const grouped = groupPredicatesByField(flatPredicates) // Simplify each group const simplified: Array<BasicExpression<boolean>> = [] for (const [field, preds] of grouped.entries()) { if (field === null) { // Complex predicates that we can't group by field simplified.push(...preds) } else { // Try to simplify same-field predicates const result = simplifyFn(preds) // For intersection: check for empty set (contradiction) if ( operation === `and` && result && result.type === `val` && result.value === false ) { // Intersection is empty (conflicting constraints) - entire AND is false return { type: `val`, value: false } as BasicExpression<boolean> } // For union: result may be null if simplification failed if (result) { simplified.push(result) } } } if (simplified.length === 0) { return { type: `val`, value: identityValue } as BasicExpression<boolean> } if (simplified.length === 1) { return simplified[0]! } // Return combined predicate return { type: `func`, name: operation, args: simplified, } as BasicExpression<boolean> } /** * Combine multiple where predicates with OR logic (union). * Returns a predicate that is satisfied when any input predicate is satisfied. * Simplifies when possible (e.g., age > 10 OR age > 20 → age > 10). * * @example * // Take least restrictive * unionWherePredicates([gt(ref('age'), val(10)), gt(ref('age'), val(20))]) // age > 10 * * @example * // Combine equals into IN * unionWherePredicates([eq(ref('age'), val(5)), eq(ref('age'), val(10))]) // age IN [5, 10] * * @param predicates - Array of where predicates to union * @returns Combined predicate representing the union */ export function unionWherePredicates( predicates: Array<BasicExpression<boolean>>, ): BasicExpression<boolean> { return combineWherePredicates(predicates, `or`, unionSameFieldPredicates) } /** * Compute the difference between two where predicates: `fromPredicate AND NOT(subtractPredicate)`. * Returns the simplified predicate, or null if the difference cannot be simplified * (in which case the caller should fetch the full fromPredicate). * * @example * // Range difference * minusWherePredicates( * gt(ref('age'), val(10)), // age > 10 * gt(ref('age'), val(20)) // age > 20 * ) // → age > 10 AND age <= 20 * * @example * // Set difference * minusWherePredicates( * inOp(ref('status'), ['A', 'B', 'C', 'D']), // status IN ['A','B','C','D'] * inOp(ref('status'), ['B', 'C']) // status IN ['B','C'] * ) // → status IN ['A', 'D'] * * @example * // Common conditions * minusWherePredicates( * and(gt(ref('age'), val(10)), eq(ref('status'), val('active'))), // age > 10 AND status = 'active' * and(gt(ref('age'), val(20)), eq(ref('status'), val('active'))) // age > 20 AND status = 'active' * ) // → age > 10 AND age <= 20 AND status = 'active' * * @example * // Complete overlap - empty result * minusWherePredicates( * gt(ref('age'), val(20)), // age > 20 * gt(ref('age'), val(10)) // age > 10 * ) // → {type: 'val', value: false} (empty set) * * @param fromPredicate - The predicate to subtract from * @param subtractPredicate - The predicate to subtract * @returns The simplified difference, or null if cannot be simplified */ export function minusWherePredicates( fromPredicate: BasicExpression<boolean> | undefined, subtractPredicate: BasicExpression<boolean> | undefined, ): BasicExpression<boolean> | null { // If nothing to subtract, return the original if (subtractPredicate === undefined) { return ( fromPredicate ?? ({ type: `val`, value: true } as BasicExpression<boolean>) ) } // If from is undefined then we are asking for all data // so we need to load all data minus what we already loaded // i.e. we need to load NOT(subtractPredicate) if (fromPredicate === undefined) { return { type: `func`, name: `not`, args: [subtractPredicate], } as BasicExpression<boolean> } // Check if fromPredicate is entirely contained in subtractPredicate // In that case, fromPredicate AND NOT(subtractPredicate) = empty set if (isWhereSubset(fromPredicate, subtractPredicate)) { return { type: `val`, value: false } as BasicExpression<boolean> } // Try to detect and handle common conditions const commonConditions = findCommonConditions( fromPredicate, subtractPredicate, ) if (commonConditions.length > 0) { // Extract predicates without common conditions const fromWithoutCommon = removeConditions(fromPredicate, commonConditions) const subtractWithoutCommon = removeConditions( subtractPredicate, commonConditions, ) // Recursively compute difference on simplified predicates const simplifiedDifference = minusWherePredicates( fromWithoutCommon, subtractWithoutCommon, ) if (simplifiedDifference !== null) { // Combine the simplified difference with common conditions return combineConditions([...commonConditions, simplifiedDifference]) } } // Check if they are on the same field - if so, we can try to simplify if (fromPredicate.type === `func` && subtractPredicate.type === `func`) { const result = minusSameFieldPredicates(fromPredicate, subtractPredicate) if (result !== null) { return result } } // Can't simplify - return null to indicate caller should fetch full fromPredicate return null } /** * Helper function to compute difference for same-field predicates */ function minusSameFieldPredicates( fromPred: Func, subtractPred: Func, ): BasicExpression<boolean> | null { // Extract field information const fromField = extractComparisonField(fromPred) || extractEqualityField(fromPred) || extractInField(fromPred) const subtractField = extractComparisonField(subtractPred) || extractEqualityField(subtractPred) || extractInField(subtractPred) // Must be on the same field if ( !fromField || !subtractField || !areRefsEqual(fromField.ref, subtractField.ref) ) { return null } // Handle IN minus IN: status IN [A,B,C,D] - status IN [B,C] = status IN [A,D] if (fromPred.name === `in` && subtractPred.name === `in`) { const fromInField = fromField as InField const subtractInField = subtractField as InField // Filter out values that are in the subtract set const remainingValues = fromInField.values.filter( (v) => !arrayIncludesWithSet( subtractInField.values, v, subtractInField.primitiveSet ?? null, subtractInField.areAllPrimitives, ), ) if (remainingValues.length === 0) { return { type: `val`, value: false } as BasicExpression<boolean> } if (remainingValues.length === 1) { return { type: `func`, name: `eq`, args: [fromField.ref, { type: `val`, value: remainingValues[0] }], } as BasicExpression<boolean> } return { type: `func`, name: `in`, args: [fromField.ref, { type: `val`, value: remainingValues }], } as BasicExpression<boolean> } // Handle IN minus equality: status IN [A,B,C] - status = B = status IN [A,C] if (fromPred.name === `in` && subtractPred.name === `eq`) { const fromInField = fromField as InField const subtractValue = (subtractField as { ref: PropRef; value: any }).value const remainingValues = fromInField.values.filter( (v) => !areValuesEqual(v, subtractValue), ) if (remainingValues.length === 0) { return { type: `val`, value: false } as BasicExpression<boolean> } if (remainingValues.length === 1) { return { type: `func`, name: `eq`, args: [fromField.ref, { type: `val`, value: remainingValues[0] }], } as BasicExpression<boolean> } return { type: `func`, name: `in`, args: [fromField.ref, { type: `val`, value: remainingValues }], } as BasicExpression<boolean> } // Handle equality minus equality: age = 15 - age = 15 = empty, age = 15 - age = 20 = age = 15 if (fromPred.name === `eq` && subtractPred.name === `eq`) { const fromValue = (fromField as { ref: PropRef; value: any }).value const subtractValue = (subtractField as { ref: PropRef; value: any }).value if (areValuesEqual(fromValue, subtractValue)) { return { type: `val`, value: false } as BasicExpression<boolean> } // No overlap - return original return fromPred as BasicExpression<boolean> } // Handle range minus range: age > 10 - age > 20 = age > 10 AND age <= 20 const fromComp = extractComparisonField(fromPred) const subtractComp = extractComparisonField(subtractPred) if ( fromComp && subtractComp && areRefsEqual(fromComp.ref, subtractComp.ref) ) { // Try to compute the difference using range logic const result = minusRangePredicates( fromPred, fromComp.value, subtractPred, subtractComp.value, ) return result } // Can't simplify return null } /** * Helper to compute difference between range predicates */ function minusRangePredicates( fromFunc: Func, fromValue: any, subtractFunc: Func, subtractValue: any, ): BasicExpression<boolean> | null { const fromOp = fromFunc.name as `gt` | `gte` | `lt` | `lte` | `eq` const subtractOp = subtractFunc.name as `gt` | `gte` | `lt` | `lte` | `eq` const ref = (extractComparisonField(fromFunc) || extractEqualityField(fromFunc))!.ref // age > 10 - age > 20 = (age > 10 AND age <= 20) if (fromOp === `gt` && subtractOp === `gt`) { if (fromValue < subtractValue) { // Result is: fromValue < field <= subtractValue return { type: `func`, name: `and`, args: [ fromFunc as BasicExpression<boolean>, { type: `func`, name: `lte`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, ], } as BasicExpression<boolean> } // fromValue >= subtractValue means no overlap return fromFunc as BasicExpression<boolean> } // age >= 10 - age >= 20 = (age >= 10 AND age < 20) if (fromOp === `gte` && subtractOp === `gte`) { if (fromValue < subtractValue) { return { type: `func`, name: `and`, args: [ fromFunc as BasicExpression<boolean>, { type: `func`, name: `lt`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // age > 10 - age >= 20 = (age > 10 AND age < 20) if (fromOp === `gt` && subtractOp === `gte`) { if (fromValue < subtractValue) { return { type: `func`, name: `and`, args: [ fromFunc as BasicExpression<boolean>, { type: `func`, name: `lt`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // age >= 10 - age > 20 = (age >= 10 AND age <= 20) if (fromOp === `gte` && subtractOp === `gt`) { if (fromValue <= subtractValue) { return { type: `func`, name: `and`, args: [ fromFunc as BasicExpression<boolean>, { type: `func`, name: `lte`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // age < 30 - age < 20 = (age >= 20 AND age < 30) if (fromOp === `lt` && subtractOp === `lt`) { if (fromValue > subtractValue) { return { type: `func`, name: `and`, args: [ { type: `func`, name: `gte`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, fromFunc as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // age <= 30 - age <= 20 = (age > 20 AND age <= 30) if (fromOp === `lte` && subtractOp === `lte`) { if (fromValue > subtractValue) { return { type: `func`, name: `and`, args: [ { type: `func`, name: `gt`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, fromFunc as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // age < 30 - age <= 20 = (age > 20 AND age < 30) if (fromOp === `lt` && subtractOp === `lte`) { if (fromValue > subtractValue) { return { type: `func`, name: `and`, args: [ { type: `func`, name: `gt`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, fromFunc as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // age <= 30 - age < 20 = (age >= 20 AND age <= 30) if (fromOp === `lte` && subtractOp === `lt`) { if (fromValue >= subtractValue) { return { type: `func`, name: `and`, args: [ { type: `func`, name: `gte`, args: [ref, { type: `val`, value: subtractValue }], } as BasicExpression<boolean>, fromFunc as BasicExpression<boolean>, ], } as BasicExpression<boolean> } return fromFunc as BasicExpression<boolean> } // Can't simplify other combinations return null } /** * Check if one orderBy clause is a subset of another. * Returns true if the subset ordering requirements are satisfied by the superset ordering. * * @example * // Subset is prefix of superset * isOrderBySubset([{expr: age, asc}], [{expr: age, asc}, {expr: name, desc}]) // true * * @param subset - The ordering requirements to check * @param superset - The ordering that might satisfy the requirements * @returns true if subset is satisfied by superset */ export function isOrderBySubset( subset: OrderBy | undefined, superset: OrderBy | undefined, ): boolean { // No ordering requirement is always satisfied if (!subset || subset.length === 0) { return true } // If there's no superset ordering but subset requires ordering, not satisfied if (!superset || superset.length === 0) { return false } // Check if subset is a prefix of superset with matching expressions and compare options if (subset.length > superset.length) { return false } for (let i = 0; i < subset.length; i++) { const subClause = subset[i]! const superClause = superset[i]! // Check if expressions match if (!areExpressionsEqual(subClause.expression, superClause.expression)) { return false } // Check if compare options match if ( !areCompareOptionsEqual( subClause.compareOptions, superClause.compareOptions, ) ) { return false } } return true } /** * Check if one limit is a subset of another. * Returns true if the subset limit requirements are satisfied by the superset limit. * * Note: This function does NOT consider offset. For offset-aware subset checking, * use `isOffsetLimitSubset` instead. * * @example * isLimitSubset(10, 20) // true (requesting 10 items when 20 are available) * isLimitSubset(20, 10) // false (requesting 20 items when only 10 are available) * isLimitSubset(10, undefined) // true (requesting 10 items when unlimited are available) * * @param subset - The limit requirement to check * @param superset - The limit that might satisfy the requirement * @returns true if subset is satisfied by superset */ export function isLimitSubset( subset: number | undefined, superset: number | undefined, ): boolean { // Unlimited superset satisfies any limit requirement if (superset === undefined) { return true } // If requesting all data (no limit), we need unlimited data to satisfy it // But we know superset is not unlimited so we return false if (subset === undefined) { return false } // Otherwise, subset must be less than or equal to superset return subset <= superset } /** * Check if one offset+limit range is a subset of another. * Returns true if the subset range is fully contained within the superset range. * * A query with `{limit: 10, offset: 0}` loads rows [0, 10). * A query with `{limit: 10, offset: 20}` loads rows [20, 30). * * For subset to be satisfied by superset: * - Superset must start at or before subset (superset.offset <= subset.offset) * - Superset must end at or after subset (superset.offset + superset.limit >= subset.offset + subset.limit) * * @example * isOffsetLimitSubset({ offset: 0, limit: 5 }, { offset: 0, limit: 10 }) // true * isOffsetLimitSubset({ offset: 5, limit: 5 }, { offset: 0, limit: 10 }) // true (rows 5-9 within 0-9) * isOffsetLimitSubset({ offset: 5, limit: 10 }, { offset: 0, limit: 10 }) // false (rows 5-14 exceed 0-9) * isOffsetLimitSubset({ offset: 20, limit: 10 }, { offset: 0, limit: 10 }) // false (rows 20-29 outside 0-9) * * @param subset - The offset+limit requirements to check * @param superset - The offset+limit that might satisfy the requirements * @returns true if subset range is fully contained within superset range */ export function isOffsetLimitSubset( subset: { offset?: number; limit?: number }, superset: { offset?: number; limit?: number }, ): boolean { const subsetOffset = subset.offset ?? 0 const supersetOffset = superset.offset ?? 0 // Superset must start at or before subset if (supersetOffset > subsetOffset) { return false } // If superset is unlimited, it covers everything from its offset onwards if (superset.limit === undefined) { return true } // If subset is unlimited but superset has a limit, subset can't be satisfied if (subset.limit === undefined) { return false } // Both have limits - check if subset range is within superset range const subsetEnd = subsetOffset + subset.limit const supersetEnd = supersetOffset + superset.limit return subsetEnd <= supersetEnd } /** * Check if one predicate (where + orderBy + limit + offset) is a subset of another. * Returns true if all aspects of the subset predicate are satisfied by the superset. * * @example * isPredicateSubset( * { where: gt(ref('age'), val(20)), limit: 10 }, * { where: gt(ref('age'), val(10)), limit: 20 } * ) // true * * @param subset - The predicate requirements to check * @param superset - The predicate that might satisfy the requirements * @returns true if subset is satisfied by superset */ export function isPredicateSubset( subset: LoadSubsetOptions, superset: LoadSubsetOptions, ): boolean { // When the superset has a limit, we can only determine subset relationship // if the where clauses are equal (not just subset relationship). // // This is because a limited query only loads a portion of the matching rows. // A more restrictive where clause might require rows outside that portion. // // Example: superset = {where: undefined, limit: 10, orderBy: desc} // subset = {where: LIKE 'search%', limit: 10, orderBy: desc} // The top 10 items matching 'search%' might include items outside the overall top 10. // // However, if the where clauses are equal, then the subset relationship can // be determined by orderBy, limit, and offset: // Example: superset = {where: status='active', limit: 10, offset: 0, orderBy: desc} // subset = {where: status='active', limit: 5, offset: 0, orderBy: desc} // The top 5 active items ARE contained in the top 10 active items. if (superset.limit !== undefined) { // For limited supersets, where clauses must be equal if (!areWhereClausesEqual(subset.where, superset.where)) { return false } return ( isOrderBySubset(subset.orderBy, superset.orderBy) && isOffsetLimitSubset(subset, superset) ) } // For unlimited supersets, use the normal subset logic // Still need to consider offset - an unlimited query with offset only covers // rows from that offset onwards return ( isWhereSubset(subset.where, superset.where) && isOrderBySubset(subset.orderBy, superset.orderBy) && isOffsetLimitSubset(subset, superset) ) } /** * Check if two where clauses are structurally equal. * Used for limited query subset checks where subset relationship isn't sufficient. */ function areWhereClausesEqual( a: BasicExpression<boolean> | undefined, b: BasicExpression<boolean> | undefined, ): boolean { if (a === undefined && b === undefined) { return true } if (a === undefined || b === undefined) { return false } return areExpressionsEqual(a, b) } // ============================================================================ // Helper functions // ============================================================================ /** * Find common conditions between two predicates. * Returns an array of conditions that appear in both predicates. */ function findCommonConditions( predicate1: BasicExpression<boolean>, predicate2: BasicExpression<boolean>, ): Array<BasicExpression<boolean>> { const conditions1 = extractAllConditions(predicate1) const conditions2 = extractAllConditions(predicate2) const common: Array<BasicExpression<boolean>> = [] for (const cond1 of conditions1) { for (const cond2 of conditions2) { if (areExpressionsEqual(cond1, cond2)) { // Avoid duplicates if (!common.some((c) => areExpressionsEqual(c, cond1))) { common.push(cond1) } break } } } return common } /** * Extract all individual conditions from a predicate, flattening AND operations. */ function extractAllConditions( predicate: BasicExpression<boolean>, ): Array<BasicExpression<boolean>> { if (predicate.type === `func` && predicate.name === `and`) { const conditions: Array<BasicExpression<boolean>> = [] for (const arg of predicate.args) { conditions.push(...extractAllConditions(arg as BasicExpression<boolean>)) } return conditions } return [predicate] } /** * Remove specified conditions from a predicate. * Returns the predicate with the specified conditions removed, or undefined if all conditions are removed. */ function removeConditions( predicate: BasicExpression<boolean>, conditionsToRemove: Array<BasicExpression<boolean>>, ): BasicExpression<boolean> | undefined { if (predicate.type === `func` && predicate.name === `and`) { const remainingArgs = predicate.args.filter( (arg) => !conditionsToRemove.some((cond) => areExpressionsEqual(arg as BasicExpression<boolean>, cond), ), ) if (remainingArgs.length === 0) { return undefined } else if (remainingArgs.length === 1) { return remainingArgs[0]! } else { return { type: `func`, name: `and`, args: remainingArgs, } as BasicExpression<boolean> } } // For non-AND predicates, don't remove anything return predicate } /** * Combine multiple conditions into a single predicate using AND logic. * Flattens nested AND operations to avoid unnecessary nesting. */ function combineConditions( conditions: Array<BasicExpression<boolean>>, ): BasicExpression<boolean> { if (conditions.length === 0) { return { type: `val`, value: true } as BasicExpression<boolean> } else if (conditions.length === 1) { return conditions[0]! } else { // Flatten all conditions, including those that are already AND operations const flattenedConditions: Array<BasicExpression<boolean>> = [] for (const condition of conditions) { if (condition.type === `func` && condition.name === `and`) { // Flatten nested AND operations flattenedConditions.push(...condition.args) } else { flattenedConditions.push(condition) } } if (flattenedConditions.length === 1) { return flattenedConditions[0]! } else { return { type: `func`, name: `and`, args: flattenedConditions, } as BasicExpression<boolean> } } } /** * Find a predicate with a specific operator and value */ function findPredicateWithOperator( predicates: Array<BasicExpression<boolean>>, operator: string, value: any, ): BasicExpression<boolean> | undefined { return predicates.find((p) => { if (p.type === `func`) { const f = p as Func const field = extractComparisonField(f) return f.name === operator && field && areValuesEqual(field.value, value) } return false }) } function areExpressionsEqual(a: BasicExpression, b: BasicExpression): boolean { if (a.type !== b.type) { return false } if (a.type === `val` && b.type === `val`) { return areValuesEqual(a.value, b.value) } if (a.type === `ref` && b.type === `ref`) { return areRefsEqual(a, b) } if (a.type === `func` && b.type === `func`) { const aFunc = a const bFunc = b if (aFunc.name !== bFunc.name) { return false } if (aFunc.args.length !== bFunc.args.length) { return false } return aFunc.args.every((arg, i) => areExpressionsEqual(arg, bFunc.args[i]!), ) } return false } function areValuesEqual(a: any, b: any): boolean { // Simple equality check - could be enhanced for deep object comparison if (a === b) { return true } // Handle NaN if (typeof a === `number` && typeof b === `number` && isNaN(a) && isNaN(b)) { return true } // Handle Date objects if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime() } // For arrays and objects, use reference equality // (In practice, we don't need deep equality for these cases - // same object reference means same value for our use case) if ( typeof a === `object` && typeof b === `object` && a !== null && b !== null ) { return a === b } return false } function areRefsEqual(a: PropRef, b: PropRef): boolean { if (a.path.length !== b.path.length) { return false } return a.path.every((segment, i) => segment === b.path[i]) } /** * Check if a value is a primitive (string, number, boolean, null, undefined) * Primitives can use Set for fast lookups */ function isPrimitive(value: any): boolean { return ( value === null || value === undefined || typeof value === `string` || typeof value === `number` || typeof value === `boolean` ) } /** * Check if all values in an array are primitives */ function areAllPrimitives(values: Array<any>): boolean { return values.every(isPrimitive) } /** * Check if a value is in an array, with optional pre-built Set for optimization. * The primitiveSet is cached in InField during extraction and reused for all lookups. */ function arrayIncludesWithSet( array: Array<any>, value: any, primitiveSet: Set<any> | null, arrayIsAllPrimitives?: boolean, ): boolean { // Fast path: use pre-built Set for O(1) lookup if (primitiveSet) { // Skip isPrimitive check if we know the value must be primitive for a match // (if array is all primitives, only primitives can match) if (arrayIsAllPrimitives || isPrimitive(value)) { return primitiveSet.has(value) } return false // Non-primitive can't be in primitive-only set } // Fallback: use areValuesEqual for Dates and objects return array.some((v) => areValuesEqual(v, value)) } /** * Get the maximum of two values, handling both numbers and Dates */ function maxValue(a: any, b: any): any { if (a instanceof Date && b instanceof Date) { return a.getTime() > b.getTime() ? a : b } return Math.max(a, b) } /** * Get the minimum of two values, handling both numbers and Dates */ function minValue(a: any, b: any): any { if (a instanceof Date && b instanceof Date) { return a.getTime() < b.getTime() ? a : b } return Math.min(a, b) } function areCompareOptionsEqual( a: { direction?: `asc` | `desc`; [key: string]: any }, b: { direction?: `asc` | `desc`; [key: string]: any }, ): boolean { // For now, just compare direction - could be enhanced for other options return a.direction === b.direction } interface ComparisonField { ref: PropRef value: any } function extractComparisonField(func: Func): ComparisonField | null { // Handle comparison operators: eq, gt, gte, lt, lte if ([`eq`, `gt`, `gte`, `lt`, `lte`].includes(func.name)) { // Assume first arg is ref, second is value const firstArg = func.args[0] const secondArg = func.args[1] if (firstArg?.type === `ref` && secondArg?.type === `val`) { return { ref: firstArg, value: secondArg.value, } } } return null } function extractEqualityField(func: Func): ComparisonField | null { if (func.name === `eq`) { const firstArg = func.args[0] const secondArg = func.args[1] if (firstArg?.type === `ref` && secondArg?.type === `val`) { return { ref: firstArg, value: secondArg.value, } } } return null } interface InField { ref: PropRef values: Array<any> // Cached optimization data (computed once, reused many times) areAllPrimitives?: boolean primitiveSet?: Set<any> | null } function extractInField(func: Func): InField | null { if (func.name === `in`) { const firstArg = func.args[0] const secondArg = func.args[1] if ( firstArg?.type === `ref` && secondArg?.type === `val` && Array.isArray(secondArg.value) ) { let values = secondArg.value // Precompute optimization metadata once const allPrimitives = areAllPrimitives(values) let primitiveSet: Set<any> | null = null if (allPrimitives && values.length > 10) { // Build Set and dedupe values at the same time primitiveSet = new Set(values) // If we found duplicates, use the deduped array going forward if (primitiveSet.size < values.length) { values = Array.from(primitiveSet) } } return { ref: firstArg, values, areAllPrimitives: allPrimitives, primitiveSet, } } } return null } function isComparisonSubset( subsetFunc: Func, subsetValue: any, supersetFunc: Func, supersetValue: any, ): boolean { const subOp = subsetFunc.name const superOp = supersetFunc.name // Handle same operator if (subOp === superOp) { if (subOp === `eq`) { // field = X is subset of field = X only // Fast path: primitives can use strict equality if (isPrimitive(subsetValue) && isPrimitive(supersetValue)) { return subsetValue === supersetValue } return areValuesEqual(subsetValue, supersetValue) } else if (subOp === `gt`) { // field > 20 is subset of field > 10 if 20 > 10 return subsetValue >= supersetValue } else if (subOp === `gte`) { // field >= 20 is subset of field >= 10 if 20 >= 10 return subsetValue >= supersetValue } else if (subOp === `lt`) { // field < 10 is subset of field < 20 if 10 <= 20 return subsetValue <= supersetValue } else if (subOp === `lte`) { // field <= 10 is subset of field <= 20 if 10 <= 20 return subsetValue <= supersetValue } } // Handle different operators on same field // eq vs gt/gte: field = 15 is subset of field > 10 if 15 > 10 if (subOp === `eq` && superOp === `gt`) { return subsetValue > supersetValue } if (subOp === `eq` && superOp === `gte`) { return subsetValue >= supersetValue } if (subOp === `eq` && superOp === `lt`) { return subsetValue < supersetValue } if (subOp === `eq` && superOp === `lte`) { return subsetValue <= supersetValue } // gt/gte vs gte/gt if (subOp === `gt` && superOp === `gte`) { // field > 10 is subset of field >= 10 if 10 >= 10 (always true for same value) return subsetValue >= supersetValue } if (subOp === `gte` && superOp === `gt`) { // field >= 11 is subset of field > 10 if 11 > 10 return subsetValue > supersetValue } // lt/lte vs lte/lt if (subOp === `lt` && superOp === `lte`) { // field < 10 is subset of field <= 10 if 10 <= 10 return subsetValue <= supersetValue } if (subOp === `lte` && superOp === `lt`) { // field <= 9 is subset of field < 10 if 9 < 10 return subsetValue < supersetValue } return false } function groupPredicatesByField( predicates: Array<BasicExpression<boolean>>, ): Map<string | null, Array<BasicExpression<boolean>>> { const groups = new Map<string | null, Array<BasicExpression<boolean>>>() for (const pred of predicates) { let fieldKey: string | null = null if (pred.type === `func`) { const func = pred as Func const field = extractComparisonField(func) || extractEqualityField(func) || extractInField(func) if (field) { fieldKey = field.ref.path.join(`.`) } } const group = groups.get(fieldKey) || [] group.push(pred) groups.set(fieldKey, group) } return groups } function unionSameFieldPredicates( predicates: Array<BasicExpression<boolean>>, ): BasicExpression<boolean> | null { if (predicates.length === 1) { return predicates[0]! } // Try to extract range constraints let maxGt: number | null = null let maxGte: number | null = null let minLt: number | null = null let minLte: number | null = null const eqValues: Set<any> = new Set() const inValues: Set<any> = new Set() const otherPredicates: Array<BasicExpression<boolean>> = [] for (const pred of predicates) { if (pred.type === `func`) { const func = pred as Func const field = extractComparisonField(func) if (field) { const value = field.value if (func.name === `gt`) { maxGt = maxGt === null ? value : minValue(maxGt, value) } else if (func.name === `gte`) { maxGte = maxGte === null ? value : minValue(maxGte, value) } else if (func.name === `lt`) { minLt = minLt === null ? value : maxValue(minLt, value) } else if (func.name === `lte`) { minLte = minLte === null ? value : maxValue(minLte, value) } else if (func.name === `eq`) { eqValues.add(value) } else { otherPredicates.push(pred) } } else { const inField = extractInField(func) if (inField) { for (const val of inField.values) { inValues.add(val) } } else { otherPredicates.push(pred) } } } else { otherPredicates.push(pred) } } // If we have multiple equality values, combine into IN if (eqValues.size > 1 || (eqValues.size > 0 && inValues.size > 0)) { const allValues = [...eqValues, ...inValues] const ref = predicates.find((p) => { if (p.type === `func`) { const field = extractComparisonField(p as Func) || extractInField(p as Func) return field !== null } return false }) if (ref && ref.type === `func`) { const field = extractComparisonField(ref as Func) || extractInField(ref as Func) if (field) { return { type: `func`, name: `in`, args: [ field.ref, { type: `val`, value: allValues } as BasicExpression, ], } as BasicExpression<boolean> } } } // Build the least restrictive range const result: Array<BasicExpression<boolean>> = [] // Choose the least restrictive lower bound if (maxGt !== null && maxGte !== null) { // Take the smaller one (less restrictive) const pred = maxGte <= maxGt ? findPredicateWithOperator(predicates, `gte`, maxGte) : findPredicateWithOperator(predicates, `gt`, maxGt) if (pred) result.push(pred) } else if (maxGt !== null) { const pred = findPredicateWithOperator(predicates, `gt`, maxGt) if (pred) result.push(pred) } else if (maxGte !== null) { const pred = findPredicateWithOperator(predicates, `gte`, maxGte) if (pred) result.push(pred) } // Choose the least restrictive upper bound if (minLt !== null && minLte !== null) { const pred = minLte >= minLt ? findPredicateWithOperator(predicates, `lte`, minLte) : findPredicateWithOperator(predicates, `lt`, minLt) if (pred) result.push(pred) } else if (minLt !== null) { const pred = findPredicateWithOperator(predicates, `lt`, minLt) if (pred) result.push(pred) } else if (minLte !== null) { const pred = findPredicateWithOperator(predicates, `lte`, minLte) if (pred) result.push(pred) } // Add single eq value if (eqValues.size === 1 && inValues.size === 0) { const pred = findPredicateWithOperator(predicates, `eq`, [...eqValues][0]) if (pred) result.push(pred) } // Add IN if only IN values if (eqValues.size === 0 && inValues.size > 0) { result.push( predicates.find((p) => { if (p.type === `func`) { return (p as Func).name === `in` } return false })!, ) } // Add other predicates result.push(...otherPredicates) if (result.length === 0) { return { type: `val`, value: true } as BasicExpression<boolean> } if (result.length === 1) { return result[0]! } return { type: `func`, name: `or`, args: result, } as BasicExpression<boolean> }