@@ -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 */
224283function 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+
441550function 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