import { compileSingleRowExpression } from '../query/compiler/evaluators.js' import { comparisonFunctions } from '../query/builder/functions.js' import { DEFAULT_COMPARE_OPTIONS, deepEquals } from '../utils.js' import type { RangeQueryOptions } from './btree-index.js' import type { CompareOptions } from '../query/builder/types.js' import type { BasicExpression, OrderByDirection } from '../query/ir.js' /** * Operations that indexes can support, imported from available comparison functions */ export const IndexOperation = comparisonFunctions /** * Type for index operation values */ export type IndexOperation = (typeof comparisonFunctions)[number] /** * Statistics about index usage and performance */ export interface IndexStats { readonly entryCount: number readonly lookupCount: number readonly averageLookupTime: number readonly lastUpdated: Date } export interface IndexInterface< TKey extends string | number = string | number, > { add: (key: TKey, item: any) => void remove: (key: TKey, item: any) => void update: (key: TKey, oldItem: any, newItem: any) => void build: (entries: Iterable<[TKey, any]>) => void clear: () => void lookup: (operation: IndexOperation, value: any) => Set equalityLookup: (value: any) => Set inArrayLookup: (values: Array) => Set rangeQuery: (options: RangeQueryOptions) => Set rangeQueryReversed: (options: RangeQueryOptions) => Set take: ( n: number, from: TKey, filterFn?: (key: TKey) => boolean, ) => Array takeFromStart: (n: number, filterFn?: (key: TKey) => boolean) => Array takeReversed: ( n: number, from: TKey, filterFn?: (key: TKey) => boolean, ) => Array takeReversedFromEnd: ( n: number, filterFn?: (key: TKey) => boolean, ) => Array get keyCount(): number get orderedEntriesArray(): Array<[any, Set]> get orderedEntriesArrayReversed(): Array<[any, Set]> get indexedKeysSet(): Set get valueMapData(): Map> supports: (operation: IndexOperation) => boolean matchesField: (fieldPath: Array) => boolean matchesCompareOptions: (compareOptions: CompareOptions) => boolean matchesDirection: (direction: OrderByDirection) => boolean getStats: () => IndexStats } /** * Base abstract class that all index types extend */ export abstract class BaseIndex< TKey extends string | number = string | number, > implements IndexInterface { public readonly id: number public readonly name?: string public readonly expression: BasicExpression public abstract readonly supportedOperations: Set protected lookupCount = 0 protected totalLookupTime = 0 protected lastUpdated = new Date() protected compareOptions: CompareOptions constructor( id: number, expression: BasicExpression, name?: string, options?: any, ) { this.id = id this.expression = expression this.compareOptions = DEFAULT_COMPARE_OPTIONS this.name = name this.initialize(options) } // Abstract methods that each index type must implement abstract add(key: TKey, item: any): void abstract remove(key: TKey, item: any): void abstract update(key: TKey, oldItem: any, newItem: any): void abstract build(entries: Iterable<[TKey, any]>): void abstract clear(): void abstract lookup(operation: IndexOperation, value: any): Set abstract take( n: number, from: TKey, filterFn?: (key: TKey) => boolean, ): Array abstract takeFromStart( n: number, filterFn?: (key: TKey) => boolean, ): Array abstract takeReversed( n: number, from: TKey, filterFn?: (key: TKey) => boolean, ): Array abstract takeReversedFromEnd( n: number, filterFn?: (key: TKey) => boolean, ): Array abstract get keyCount(): number abstract equalityLookup(value: any): Set abstract inArrayLookup(values: Array): Set abstract rangeQuery(options: RangeQueryOptions): Set abstract rangeQueryReversed(options: RangeQueryOptions): Set abstract get orderedEntriesArray(): Array<[any, Set]> abstract get orderedEntriesArrayReversed(): Array<[any, Set]> abstract get indexedKeysSet(): Set abstract get valueMapData(): Map> // Common methods supports(operation: IndexOperation): boolean { return this.supportedOperations.has(operation) } matchesField(fieldPath: Array): boolean { return ( this.expression.type === `ref` && this.expression.path.length === fieldPath.length && this.expression.path.every((part, i) => part === fieldPath[i]) ) } /** * Checks if the compare options match the index's compare options. * The direction is ignored because the index can be reversed if the direction is different. */ matchesCompareOptions(compareOptions: CompareOptions): boolean { const thisCompareOptionsWithoutDirection = { ...this.compareOptions, direction: undefined, } const compareOptionsWithoutDirection = { ...compareOptions, direction: undefined, } return deepEquals( thisCompareOptionsWithoutDirection, compareOptionsWithoutDirection, ) } /** * Checks if the index matches the provided direction. */ matchesDirection(direction: OrderByDirection): boolean { return this.compareOptions.direction === direction } getStats(): IndexStats { return { entryCount: this.keyCount, lookupCount: this.lookupCount, averageLookupTime: this.lookupCount > 0 ? this.totalLookupTime / this.lookupCount : 0, lastUpdated: this.lastUpdated, } } protected abstract initialize(options?: any): void protected evaluateIndexExpression(item: any): any { const evaluator = compileSingleRowExpression(this.expression) return evaluator(item as Record) } protected trackLookup(startTime: number): void { const duration = performance.now() - startTime this.lookupCount++ this.totalLookupTime += duration } protected updateTimestamp(): void { this.lastUpdated = new Date() } } /** * Type for index constructor */ export type IndexConstructor = new ( id: number, expression: BasicExpression, name?: string, options?: any, ) => BaseIndex