Skip to content

Commit 8fc3efd

Browse files
committed
fix tests for query collection
1 parent 2846a6e commit 8fc3efd

File tree

1 file changed

+110
-1
lines changed

1 file changed

+110
-1
lines changed

packages/query-db-collection/e2e/query-filter.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,62 @@ export function applyPredicates<T>(
162162
): Array<T> {
163163
if (!options) return data
164164

165-
const { filters, sorts, limit } = parseLoadSubsetOptions(options)
165+
// Parse options: try simple comparisons first (faster path), fall back to expression evaluation if needed
166+
// extractSimpleComparisons (called by parseLoadSubsetOptions) intentionally throws for unsupported operators
167+
// like 'like', 'ilike', 'or', etc. When that happens, we use buildExpressionPredicate instead.
168+
let filters: Array<SimpleComparison> = []
169+
let sorts: Array<ParsedOrderBy> = []
170+
let limit: number | undefined = undefined
171+
172+
// Check if where clause is simple before trying to parse
173+
const hasComplexWhere = options.where && !isSimpleExpression(options.where)
174+
175+
if (!hasComplexWhere) {
176+
// Simple expression - parse everything at once
177+
try {
178+
const parsed = parseLoadSubsetOptions(options)
179+
filters = parsed.filters
180+
sorts = parsed.sorts
181+
limit = parsed.limit
182+
} catch (error) {
183+
// This shouldn't happen for simple expressions, but handle it gracefully
184+
if (DEBUG_SUMMARY) {
185+
console.log(
186+
`[query-filter] parseLoadSubsetOptions failed unexpectedly`,
187+
error
188+
)
189+
}
190+
limit = options.limit
191+
}
192+
} else {
193+
// Complex expression (like/ilike/or/etc.) - cannot use simple comparisons
194+
// We'll filter using buildExpressionPredicate which evaluates the full expression tree
195+
// filters stays empty - this signals buildFilterPredicate to use buildExpressionPredicate instead of buildSimplePredicate
196+
// Note: Filtering still happens! Just via a different path (expression evaluation vs simple comparisons)
197+
198+
limit = options.limit
199+
200+
if (options.orderBy) {
201+
try {
202+
const orderByParsed = parseLoadSubsetOptions({
203+
orderBy: options.orderBy,
204+
})
205+
sorts = orderByParsed.sorts
206+
} catch {
207+
// OrderBy parsing failed, will skip sorting
208+
if (DEBUG_SUMMARY) {
209+
console.log(`[query-filter] orderBy parsing failed, skipping sort`)
210+
}
211+
}
212+
}
213+
214+
if (DEBUG_SUMMARY) {
215+
console.log(
216+
`[query-filter] complex where clause detected, will filter using buildExpressionPredicate`
217+
)
218+
}
219+
}
220+
166221
if (DEBUG_SUMMARY) {
167222
const { limit: rawLimit, where, orderBy } = options
168223
const analysis = analyzeExpression(where)
@@ -220,6 +275,10 @@ export function applyPredicates<T>(
220275

221276
/**
222277
* Build a predicate function from expression tree
278+
*
279+
* Two paths:
280+
* 1. Simple expressions (eq, gt, etc.) with parsed filters -> buildSimplePredicate (faster)
281+
* 2. Complex expressions (like, ilike, or, etc.) or empty filters -> buildExpressionPredicate (full expression evaluation)
223282
*/
224283
function buildFilterPredicate<T>(
225284
where: IR.BasicExpression<boolean> | undefined,
@@ -229,10 +288,13 @@ function buildFilterPredicate<T>(
229288
return undefined
230289
}
231290

291+
// Use simple predicate if we have parsed filters (fast path for eq, gt, etc.)
232292
if (filters.length > 0 && isSimpleExpression(where)) {
233293
return buildSimplePredicate<T>(filters)
234294
}
235295

296+
// Otherwise, use expression predicate (handles like, ilike, or, etc.)
297+
// This still filters! It just evaluates the expression tree directly instead of using parsed comparisons
236298
try {
237299
return buildExpressionPredicate<T>(where)
238300
} catch (error) {
@@ -433,11 +495,58 @@ function evaluateFunction(name: string, args: Array<any>): any {
433495
return args[0] === undefined
434496
case `isNotUndefined`:
435497
return args[0] !== undefined
498+
case `like`:
499+
return evaluateLike(args[0], args[1], false)
500+
case `ilike`:
501+
return evaluateLike(args[0], args[1], true)
502+
case `lower`:
503+
return typeof args[0] === `string` ? args[0].toLowerCase() : args[0]
504+
case `upper`:
505+
return typeof args[0] === `string` ? args[0].toUpperCase() : args[0]
436506
default:
437507
throw new Error(`Unsupported predicate operator: ${name}`)
438508
}
439509
}
440510

511+
/**
512+
* Evaluates LIKE/ILIKE patterns
513+
* Converts SQL LIKE pattern to regex for JavaScript matching
514+
* Returns null for 3-valued logic (UNKNOWN) when value or pattern is null/undefined
515+
*/
516+
function evaluateLike(
517+
value: any,
518+
pattern: any,
519+
caseInsensitive: boolean
520+
): boolean | null {
521+
// In 3-valued logic, if value or pattern is null/undefined, return UNKNOWN (null)
522+
if (
523+
value === null ||
524+
value === undefined ||
525+
pattern === null ||
526+
pattern === undefined
527+
) {
528+
return null
529+
}
530+
531+
if (typeof value !== `string` || typeof pattern !== `string`) {
532+
return false
533+
}
534+
535+
const searchValue = caseInsensitive ? value.toLowerCase() : value
536+
const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern
537+
538+
// Convert SQL LIKE pattern to regex
539+
// First escape all regex special chars except % and _
540+
let regexPattern = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`)
541+
542+
// Then convert SQL wildcards to regex
543+
regexPattern = regexPattern.replace(/%/g, `.*`) // % matches any sequence
544+
regexPattern = regexPattern.replace(/_/g, `.`) // _ matches any single char
545+
546+
const regex = new RegExp(`^${regexPattern}$`)
547+
return regex.test(searchValue)
548+
}
549+
441550
function compareBySorts<T>(a: T, b: T, sorts: Array<ParsedOrderBy>): number {
442551
for (const sort of sorts) {
443552
const aVal = getFieldValue(a, sort.field)

0 commit comments

Comments
 (0)