|
| 1 | +import { OrderedIndex } from "./ordered-index" |
| 2 | +import type { BasicExpression } from "../query/ir" |
| 3 | +import type { CollectionImpl } from "../collection" |
| 4 | + |
| 5 | +export interface AutoIndexConfig { |
| 6 | + autoIndex?: `off` | `eager` |
| 7 | +} |
| 8 | + |
| 9 | +/** |
| 10 | + * Analyzes a where expression and creates indexes for all simple operations on single fields |
| 11 | + */ |
| 12 | +export function ensureIndexForExpression< |
| 13 | + T extends Record<string, any>, |
| 14 | + TKey extends string | number, |
| 15 | +>( |
| 16 | + expression: BasicExpression, |
| 17 | + collection: CollectionImpl<T, TKey, any, any, any> |
| 18 | +): void { |
| 19 | + // Only proceed if auto-indexing is enabled |
| 20 | + if (collection.config.autoIndex !== `eager`) { |
| 21 | + return |
| 22 | + } |
| 23 | + |
| 24 | + // Don't auto-index during sync operations |
| 25 | + if ( |
| 26 | + collection.status === `loading` || |
| 27 | + collection.status === `initialCommit` |
| 28 | + ) { |
| 29 | + return |
| 30 | + } |
| 31 | + |
| 32 | + // Extract all indexable expressions and create indexes for them |
| 33 | + const indexableExpressions = extractIndexableExpressions(expression) |
| 34 | + |
| 35 | + for (const { fieldName, fieldPath } of indexableExpressions) { |
| 36 | + // Check if we already have an index for this field |
| 37 | + const existingIndex = Array.from(collection.indexes.values()).find( |
| 38 | + (index) => index.matchesField(fieldPath) |
| 39 | + ) |
| 40 | + |
| 41 | + if (existingIndex) { |
| 42 | + continue // Index already exists |
| 43 | + } |
| 44 | + |
| 45 | + // Create a new index for this field using the collection's createIndex method |
| 46 | + try { |
| 47 | + collection.createIndex((row) => (row as any)[fieldName], { |
| 48 | + name: `auto_${fieldName}`, |
| 49 | + indexType: OrderedIndex, |
| 50 | + }) |
| 51 | + } catch (error) { |
| 52 | + console.warn( |
| 53 | + `Failed to create auto-index for field "${fieldName}":`, |
| 54 | + error |
| 55 | + ) |
| 56 | + } |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Extracts all indexable expressions from a where expression |
| 62 | + */ |
| 63 | +function extractIndexableExpressions( |
| 64 | + expression: BasicExpression |
| 65 | +): Array<{ fieldName: string; fieldPath: Array<string> }> { |
| 66 | + const results: Array<{ fieldName: string; fieldPath: Array<string> }> = [] |
| 67 | + |
| 68 | + function extractFromExpression(expr: BasicExpression): void { |
| 69 | + if (expr.type !== `func`) { |
| 70 | + return |
| 71 | + } |
| 72 | + |
| 73 | + const func = expr as any |
| 74 | + |
| 75 | + // Handle 'and' expressions by recursively processing all arguments |
| 76 | + if (func.name === `and`) { |
| 77 | + for (const arg of func.args) { |
| 78 | + extractFromExpression(arg) |
| 79 | + } |
| 80 | + return |
| 81 | + } |
| 82 | + |
| 83 | + // Check if this is a supported operation |
| 84 | + const supportedOperations = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`] |
| 85 | + if (!supportedOperations.includes(func.name)) { |
| 86 | + return |
| 87 | + } |
| 88 | + |
| 89 | + // Check if the first argument is a property reference (single field) |
| 90 | + if (func.args.length < 1 || func.args[0].type !== `ref`) { |
| 91 | + return |
| 92 | + } |
| 93 | + |
| 94 | + const fieldRef = func.args[0] |
| 95 | + const fieldPath = fieldRef.path |
| 96 | + |
| 97 | + // Skip if it's not a simple field (e.g., nested properties or array access) |
| 98 | + if (fieldPath.length !== 1) { |
| 99 | + return |
| 100 | + } |
| 101 | + |
| 102 | + const fieldName = fieldPath[0] |
| 103 | + results.push({ fieldName, fieldPath }) |
| 104 | + } |
| 105 | + |
| 106 | + extractFromExpression(expression) |
| 107 | + return results |
| 108 | +} |
0 commit comments