diff --git a/.changeset/nice-months-nail.md b/.changeset/nice-months-nail.md new file mode 100644 index 000000000..54ee79419 --- /dev/null +++ b/.changeset/nice-months-nail.md @@ -0,0 +1,5 @@ +--- +'@tanstack/db': patch +--- + +Fix like/ilike `%` and `_` not matching newline characters diff --git a/packages/db-collection-e2e/src/fixtures/seed-data.ts b/packages/db-collection-e2e/src/fixtures/seed-data.ts index 98b979404..818f2acf2 100644 --- a/packages/db-collection-e2e/src/fixtures/seed-data.ts +++ b/packages/db-collection-e2e/src/fixtures/seed-data.ts @@ -68,6 +68,7 @@ export function generateSeedData(): SeedDataResult { `Rose ${i}`, `sam ${i}`, `Tina ${i}`, + `Ursula ${i}\nNewline`, ] const name = names[i % names.length] diff --git a/packages/db-collection-e2e/src/suites/predicates.suite.ts b/packages/db-collection-e2e/src/suites/predicates.suite.ts index 2b3a9b216..eef75f7dc 100644 --- a/packages/db-collection-e2e/src/suites/predicates.suite.ts +++ b/packages/db-collection-e2e/src/suites/predicates.suite.ts @@ -412,6 +412,27 @@ export function createPredicatesTestSuite( await query.cleanup() }) + it(`should filter with like() with wildcard pattern matching newline`, async () => { + const config = await getConfig() + const usersCollection = config.collections.onDemand.users + + const query = createLiveQueryCollection((q) => + q + .from({ user: usersCollection }) + .where(({ user }) => like(user.name, `Ursula%`)), + ) + + await query.preload() + await waitForQueryData(query, { minSize: 1 }) + + const results = Array.from(query.state.values()) + expect(results.length).toBeGreaterThan(0) + // should match names starting with "Ursula" even if it contains a newline character + assertAllItemsMatch(query, (u) => u.name.startsWith(`Ursula`)) + + await query.cleanup() + }) + it(`should filter with like() with lower() function`, async () => { const config = await getConfig() const usersCollection = config.collections.onDemand.users diff --git a/packages/db/src/query/compiler/evaluators.ts b/packages/db/src/query/compiler/evaluators.ts index 1e2637a73..bd13aac64 100644 --- a/packages/db/src/query/compiler/evaluators.ts +++ b/packages/db/src/query/compiler/evaluators.ts @@ -516,6 +516,7 @@ function evaluateLike( regexPattern = regexPattern.replace(/%/g, `.*`) // % matches any sequence regexPattern = regexPattern.replace(/_/g, `.`) // _ matches any single char - const regex = new RegExp(`^${regexPattern}$`) + // 's' (dotAll flag) makes '.' match all characters including line terminations + const regex = new RegExp(`^${regexPattern}$`, 's') return regex.test(searchValue) } diff --git a/packages/db/tests/query/compiler/evaluators.test.ts b/packages/db/tests/query/compiler/evaluators.test.ts index 68e08d7ea..26478d04d 100644 --- a/packages/db/tests/query/compiler/evaluators.test.ts +++ b/packages/db/tests/query/compiler/evaluators.test.ts @@ -374,6 +374,23 @@ describe(`evaluators`, () => { // In 3-valued logic, ilike with undefined pattern returns UNKNOWN (null) expect(compiled({})).toBe(null) }) + + it(`like % wildcard matches across newline characters`, () => { + const func = new Func(`like`, [ + new Value(`hello\nworld`), + new Value(`%world`), + ]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(true) + }) + + it(`like _ wildcard matches a newline character`, () => { + const func = new Func(`like`, [new Value(`a\nb`), new Value(`a_b`)]) + const compiled = compileExpression(func) + + expect(compiled({})).toBe(true) + }) }) describe(`comparison operators`, () => { diff --git a/packages/query-db-collection/e2e/query-filter.ts b/packages/query-db-collection/e2e/query-filter.ts index 43f4d0e87..aa3de76b1 100644 --- a/packages/query-db-collection/e2e/query-filter.ts +++ b/packages/query-db-collection/e2e/query-filter.ts @@ -556,7 +556,7 @@ function evaluateLike( regexPattern = regexPattern.replace(/%/g, `.*`) // % matches any sequence regexPattern = regexPattern.replace(/_/g, `.`) // _ matches any single char - const regex = new RegExp(`^${regexPattern}$`) + const regex = new RegExp(`^${regexPattern}$`, 's') return regex.test(searchValue) }