Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | 5090x 8x 8x 5082x 8x 8x 5074x 5074x 5090x 5090x 624x 3245x 3245x 40x 40x 16x 16x 24x 24x 20x 24x 24x 2840x 2814x 2814x 2655x 2814x 8x 8x 4x 2167x 8x 4x 4x 2387x 2387x 3506x 551x 2955x 391x 2564x 2564x 2387x | /**
* Filter Cache Manager
* Provides per-query filter SQL caching for parameter deduplication
*
* When building queries with multiple CTEs, the same filter values can appear
* multiple times as separate SQL parameters. This cache ensures each unique
* filter is built once and reused, reducing parameter array size and improving
* query debugging.
*/
import type { SQL } from 'drizzle-orm'
import type { Filter, FilterCondition } from './types'
/**
* Generates a deterministic cache key for a filter
* Keys are based on filter content (member, operator, values) not object identity
*/
export function getFilterKey(filter: Filter): string {
// Handle logical filters recursively
if ('and' in filter) {
const subKeys = (filter.and as Filter[]).map(getFilterKey).sort()
return `and:[${subKeys.join(',')}]`
}
if ('or' in filter) {
const subKeys = (filter.or as Filter[]).map(getFilterKey).sort()
return `or:[${subKeys.join(',')}]`
}
// Simple filter: member + operator + sorted values
const fc = filter as FilterCondition
const valuesKey = JSON.stringify(
Array.isArray(fc.values) ? [...fc.values].sort() : fc.values
)
const dateRangeKey = fc.dateRange
? `:dr:${JSON.stringify(fc.dateRange)}`
: ''
return `${fc.member}:${fc.operator}:${valuesKey}${dateRangeKey}`
}
/**
* Generates a cache key for time dimension date range filters
*/
export function getTimeDimensionFilterKey(dimension: string, dateRange: string | string[]): string {
return `timeDim:${dimension}:${JSON.stringify(dateRange)}`
}
/**
* Cache statistics for debugging and monitoring
*/
export interface FilterCacheStats {
hits: number
misses: number
cacheSize: number
}
/**
* Manages filter SQL caching for a single query execution
*
* Design principles:
* - Immutable SQL: Once cached, SQL objects are never modified
* - Cache lifetime: Created at query start, discarded after query execution
* - Key by content: Filters with same member/operator/values share SQL
* - Thread-safe: No shared mutable state between queries
*/
export class FilterCacheManager {
private cache = new Map<string, SQL>()
private stats = { hits: 0, misses: 0 }
/**
* Get cached SQL or build and cache it
*
* @param key - The cache key (use getFilterKey or getTimeDimensionFilterKey)
* @param builder - Function to build the SQL if not cached
* @returns The cached or freshly built SQL, or null if builder returns null
*/
getOrBuild(key: string, builder: () => SQL | null): SQL | null {
const cached = this.cache.get(key)
if (cached !== undefined) {
this.stats.hits++
return cached
}
const sql = builder()
if (sql) {
this.cache.set(key, sql)
}
this.stats.misses++
return sql
}
/**
* Check if a key exists in the cache without affecting stats
*/
has(key: string): boolean {
return this.cache.has(key)
}
/**
* Get cached SQL without building (returns undefined if not cached)
*/
get(key: string): SQL | undefined {
const cached = this.cache.get(key)
if (cached !== undefined) {
this.stats.hits++
}
return cached
}
/**
* Pre-populate cache with multiple filters
* Useful for batch initialization at query start
*/
preload(entries: Array<{ key: string; sql: SQL }>): void {
for (const { key, sql } of entries) {
if (!this.cache.has(key)) {
this.cache.set(key, sql)
}
}
}
/**
* Store a single entry in the cache
*/
set(key: string, sql: SQL): void {
this.cache.set(key, sql)
}
/**
* Get cache statistics for debugging
*/
getStats(): FilterCacheStats {
return { ...this.stats, cacheSize: this.cache.size }
}
/**
* Clear the cache (normally not needed as cache is per-query)
*/
clear(): void {
this.cache.clear()
this.stats = { hits: 0, misses: 0 }
}
}
/**
* Flatten nested AND/OR filters into individual filter conditions
* Useful for pre-building all filters at query start
*/
export function flattenFilters(filters: Filter[]): FilterCondition[] {
const result: FilterCondition[] = []
for (const filter of filters) {
if ('and' in filter && filter.and) {
result.push(...flattenFilters(filter.and))
} else if ('or' in filter && filter.or) {
result.push(...flattenFilters(filter.or))
E} else if ('member' in filter) {
result.push(filter as FilterCondition)
}
}
return result
}
|