@@ -12,8 +12,11 @@ import {
1212 eq ,
1313 gt ,
1414 gte ,
15+ ilike ,
1516 inArray ,
1617 isNull ,
18+ like ,
19+ lower ,
1720 lt ,
1821 lte ,
1922 not ,
@@ -258,6 +261,250 @@ export function createPredicatesTestSuite(
258261 } )
259262 } )
260263
264+ describe ( `String Pattern Matching Operators` , ( ) => {
265+ it ( `should filter with like() operator (case-sensitive)` , async ( ) => {
266+ const config = await getConfig ( )
267+ const usersCollection = config . collections . onDemand . users
268+
269+ const query = createLiveQueryCollection ( ( q ) =>
270+ q
271+ . from ( { user : usersCollection } )
272+ . where ( ( { user } ) => like ( user . name , `Alice%` ) )
273+ )
274+
275+ await query . preload ( )
276+ await waitForQueryData ( query , { minSize : 1 } )
277+
278+ const results = Array . from ( query . state . values ( ) )
279+ expect ( results . length ) . toBeGreaterThan ( 0 )
280+ // Should match names starting with "Alice" (case-sensitive)
281+ assertAllItemsMatch ( query , ( u ) => u . name . startsWith ( `Alice` ) )
282+
283+ await query . cleanup ( )
284+ } )
285+
286+ it ( `should filter with ilike() operator (case-insensitive)` , async ( ) => {
287+ const config = await getConfig ( )
288+ const usersCollection = config . collections . onDemand . users
289+
290+ const query = createLiveQueryCollection ( ( q ) =>
291+ q
292+ . from ( { user : usersCollection } )
293+ . where ( ( { user } ) => ilike ( user . name , `alice%` ) )
294+ )
295+
296+ await query . preload ( )
297+ await waitForQueryData ( query , { minSize : 1 } )
298+
299+ const results = Array . from ( query . state . values ( ) )
300+ expect ( results . length ) . toBeGreaterThan ( 0 )
301+ // Should match names starting with "Alice" (case-insensitive)
302+ assertAllItemsMatch ( query , ( u ) =>
303+ u . name . toLowerCase ( ) . startsWith ( `alice` )
304+ )
305+
306+ await query . cleanup ( )
307+ } )
308+
309+ it ( `should filter with like() with wildcard pattern (% at end)` , async ( ) => {
310+ const config = await getConfig ( )
311+ const usersCollection = config . collections . onDemand . users
312+
313+ const query = createLiveQueryCollection ( ( q ) =>
314+ q
315+ . from ( { user : usersCollection } )
316+ . where ( ( { user } ) => like ( user . email , `%@example.com` ) )
317+ )
318+
319+ await query . preload ( )
320+ await waitForQueryData ( query , { minSize : 1 } )
321+
322+ const results = Array . from ( query . state . values ( ) )
323+ expect ( results . length ) . toBeGreaterThan ( 0 )
324+ // Should match emails ending with @example .com
325+ assertAllItemsMatch (
326+ query ,
327+ ( u ) => u . email ?. endsWith ( `@example.com` ) ?? false
328+ )
329+
330+ await query . cleanup ( )
331+ } )
332+
333+ it ( `should filter with like() with wildcard pattern (% in middle)` , async ( ) => {
334+ const config = await getConfig ( )
335+ const usersCollection = config . collections . onDemand . users
336+
337+ const query = createLiveQueryCollection ( ( q ) =>
338+ q
339+ . from ( { user : usersCollection } )
340+ . where ( ( { user
} ) => like ( user . email , `user%[email protected] ` ) ) 341+ )
342+
343+ await query . preload ( )
344+ await waitForQueryData ( query , { minSize : 1 } )
345+
346+ const results = Array . from ( query . state . values ( ) )
347+ expect ( results . length ) . toBeGreaterThan ( 0 )
348+ // Should match emails like user0@example .com, [email protected] , [email protected] , etc. 349+ assertAllItemsMatch (
350+ query ,
351+ ( u ) => ( u . email ?. match ( / ^ u s e r .* 0 @ e x a m p l e \. c o m $ / ) ?? null ) !== null
352+ )
353+
354+ await query . cleanup ( )
355+ } )
356+
357+ it ( `should filter with like() with lower() function` , async ( ) => {
358+ const config = await getConfig ( )
359+ const usersCollection = config . collections . onDemand . users
360+
361+ const query = createLiveQueryCollection ( ( q ) =>
362+ q
363+ . from ( { user : usersCollection } )
364+ . where ( ( { user } ) => like ( lower ( user . name ) , `%alice%` ) )
365+ )
366+
367+ await query . preload ( )
368+ await waitForQueryData ( query , { minSize : 1 } )
369+
370+ const results = Array . from ( query . state . values ( ) )
371+ expect ( results . length ) . toBeGreaterThan ( 0 )
372+ // Should match names containing "alice" (case-insensitive via lower())
373+ assertAllItemsMatch ( query , ( u ) =>
374+ u . name . toLowerCase ( ) . includes ( `alice` )
375+ )
376+
377+ await query . cleanup ( )
378+ } )
379+
380+ it ( `should filter with ilike() with lower() function` , async ( ) => {
381+ const config = await getConfig ( )
382+ const usersCollection = config . collections . onDemand . users
383+
384+ const query = createLiveQueryCollection ( ( q ) =>
385+ q
386+ . from ( { user : usersCollection } )
387+ . where ( ( { user } ) => ilike ( lower ( user . name ) , `%bob%` ) )
388+ )
389+
390+ await query . preload ( )
391+ await waitForQueryData ( query , { minSize : 1 } )
392+
393+ const results = Array . from ( query . state . values ( ) )
394+ expect ( results . length ) . toBeGreaterThan ( 0 )
395+ // Should match names containing "bob" (case-insensitive)
396+ assertAllItemsMatch ( query , ( u ) => u . name . toLowerCase ( ) . includes ( `bob` ) )
397+
398+ await query . cleanup ( )
399+ } )
400+
401+ it ( `should filter with or() combining multiple like() conditions (search pattern)` , async ( ) => {
402+ const config = await getConfig ( )
403+ const postsCollection = config . collections . onDemand . posts
404+
405+ // This mimics the user's exact query pattern with multiple fields
406+ // User's pattern: like(lower(offers.title), `%${searchLower}%`) OR like(lower(offers.human_id), `%${searchLower}%`)
407+ const searchTerm = `Introduction`
408+ const searchLower = searchTerm . toLowerCase ( )
409+
410+ const query = createLiveQueryCollection ( ( q ) =>
411+ q
412+ . from ( { post : postsCollection } )
413+ . where ( ( { post } ) =>
414+ or (
415+ like ( lower ( post . title ) , `%${ searchLower } %` ) ,
416+ like ( lower ( post . content ?? `` ) , `%${ searchLower } %` )
417+ )
418+ )
419+ )
420+
421+ await query . preload ( )
422+ await waitForQueryData ( query , { minSize : 1 } )
423+
424+ const results = Array . from ( query . state . values ( ) )
425+ expect ( results . length ) . toBeGreaterThan ( 0 )
426+ // Should match posts with title or content containing "introduction" (case-insensitive)
427+ assertAllItemsMatch (
428+ query ,
429+ ( p ) =>
430+ p . title . toLowerCase ( ) . includes ( searchLower ) ||
431+ ( p . content ?. toLowerCase ( ) . includes ( searchLower ) ?? false )
432+ )
433+
434+ await query . cleanup ( )
435+ } )
436+
437+ it ( `should filter with like() and orderBy` , async ( ) => {
438+ const config = await getConfig ( )
439+ const usersCollection = config . collections . onDemand . users
440+
441+ const query = createLiveQueryCollection ( ( q ) =>
442+ q
443+ . from ( { user : usersCollection } )
444+ . where ( ( { user } ) => like ( lower ( user . name ) , `%alice%` ) )
445+ . orderBy ( ( { user } ) => user . name , `asc` )
446+ )
447+
448+ await query . preload ( )
449+ await waitForQueryData ( query , { minSize : 1 } )
450+
451+ const results = Array . from ( query . state . values ( ) )
452+ expect ( results . length ) . toBeGreaterThan ( 0 )
453+ assertAllItemsMatch ( query , ( u ) =>
454+ u . name . toLowerCase ( ) . includes ( `alice` )
455+ )
456+
457+ // Verify ordering
458+ const names = results . map ( ( u ) => u . name )
459+ const sortedNames = [ ...names ] . sort ( ( a , b ) => a . localeCompare ( b ) )
460+ expect ( names ) . toEqual ( sortedNames )
461+
462+ await query . cleanup ( )
463+ } )
464+
465+ it ( `should filter with like() and limit` , async ( ) => {
466+ const config = await getConfig ( )
467+ const usersCollection = config . collections . onDemand . users
468+
469+ const query = createLiveQueryCollection ( ( q ) =>
470+ q
471+ . from ( { user : usersCollection } )
472+ . where ( ( { user } ) => like ( lower ( user . name ) , `%alice%` ) )
473+ . orderBy ( ( { user } ) => user . name , `asc` ) // Required when using LIMIT
474+ . limit ( 5 )
475+ )
476+
477+ await query . preload ( )
478+ await waitForQueryData ( query , { minSize : 1 } )
479+
480+ const results = Array . from ( query . state . values ( ) )
481+ // Should respect limit
482+ expect ( results . length ) . toBeLessThanOrEqual ( 5 )
483+ assertAllItemsMatch ( query , ( u ) =>
484+ u . name . toLowerCase ( ) . includes ( `alice` )
485+ )
486+
487+ await query . cleanup ( )
488+ } )
489+
490+ it ( `should handle like() with pattern matching no records` , async ( ) => {
491+ const config = await getConfig ( )
492+ const usersCollection = config . collections . onDemand . users
493+
494+ const query = createLiveQueryCollection ( ( q ) =>
495+ q
496+ . from ( { user : usersCollection } )
497+ . where ( ( { user } ) => like ( user . name , `NonExistent%` ) )
498+ )
499+
500+ await query . preload ( )
501+
502+ assertCollectionSize ( query , 0 )
503+
504+ await query . cleanup ( )
505+ } )
506+ } )
507+
261508 describe ( `In Operator` , ( ) => {
262509 it ( `should filter with inArray() on string array` , async ( ) => {
263510 const config = await getConfig ( )
0 commit comments