Skip to content

fix(zqlite): Postgres-correct LIKE/ILIKE on the SQLite replica - #6095

Merged
arv merged 3 commits into
rocicorp:mainfrom
arv:arv/zqlite-unicode-like-ilike
Jun 1, 2026
Merged

fix(zqlite): Postgres-correct LIKE/ILIKE on the SQLite replica#6095
arv merged 3 commits into
rocicorp:mainfrom
arv:arv/zqlite-unicode-like-ilike

Conversation

@arv

@arv arv commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Problem

The SQLite replica (zqlite) diverged from Postgres on LIKE/ILIKE, so query results could differ between the server-side replica, Postgres, and the in-memory IVM matcher:

  • Postgres LIKE is case-sensitive, but SQLite's LIKE operator is case-insensitive by default → plain LIKE matched too much.
  • ILIKE was rewritten to LIKE, which only case-folds ASCII → non-English ILIKE was effectively case-sensitive (e.g. Ä/ä, Cyrillic, Greek didn't match).
  • No ESCAPE was emitted, but Postgres and the in-memory IVM matcher (zql/src/builder/like.ts) both treat backslash as the default escape character.

Fix

Mirror the in-memory IVM matcher so all three backends (Postgres, IVM, SQLite replica) agree:

  • Enable PRAGMA case_sensitive_like = ON on every connection (db.ts) so the bare LIKE operator is case-sensitive — matching Postgres LIKE.
  • Compile ILIKE/NOT ILIKE as lower(a) LIKE lower(b), using the Unicode-aware lower() that @rocicorp/zero-sqlite3 provides via ICU — matching the toLowerCase() the IVM matcher uses.
  • Emit an explicit ESCAPE '\' for all LIKE/ILIKE operators.

The generated SQL now looks like:

Op SQL
LIKE "name" LIKE ? ESCAPE '\'
NOT LIKE "name" NOT LIKE ? ESCAPE '\'
ILIKE lower("name") LIKE lower(?) ESCAPE '\'
NOT ILIKE lower("name") NOT LIKE lower(?) ESCAPE '\'

Dependency note

Full Unicode case-insensitivity for ILIKE requires the ICU-enabled build of @rocicorp/zero-sqlite3 (see companion PR rocicorp/zero-sqlite3#31, which compiles SQLite with SQLITE_ENABLE_ICU). Until that release is picked up:

  • LIKE case-sensitivity and ESCAPE '\' take effect immediately (core SQLite).
  • ILIKE remains ASCII-folded via lower() — i.e. no regression vs. today, and it upgrades to full Unicode automatically once the ICU build lands.

Safety of the global pragma

case_sensitive_like = ON is connection-scoped. The only internal SQLite LIKEs are the lowercase introspection patterns in lite-tables.ts ('sqlite_%', '_zero.%'), which match lowercase identifiers and stay correct under case-sensitivity. All other internal LIKE usage is in Postgres queries, which the pragma doesn't affect.

Tests

  • New unit tests in query-builder.test.ts pin the generated SQL for all four operators.
  • Existing table-source.test.ts / query.test.ts / db.test.ts pass (45 tests).
  • tsc and oxfmt clean.

🤖 Generated with Claude Code

The SQLite replica diverged from Postgres on pattern matching:

  * Postgres LIKE is case-sensitive, but SQLite's LIKE operator is
    case-insensitive by default, so plain LIKE matched too much.
  * ILIKE was rewritten to LIKE, which only case-folds ASCII — non-English
    ILIKE was effectively case-sensitive.
  * No ESCAPE was emitted, but Postgres (and the in-memory IVM matcher in
    zql/src/builder/like.ts) treat backslash as the default escape.

Fix, mirroring the IVM matcher so all three backends agree:

  * Enable `PRAGMA case_sensitive_like = ON` on every connection so the bare
    LIKE operator is case-sensitive (= Postgres LIKE).
  * Compile ILIKE/NOT ILIKE as `lower(a) LIKE lower(b)`, using the
    Unicode-aware lower() that @rocicorp/zero-sqlite3 provides via ICU.
  * Emit an explicit `ESCAPE '\'` for all LIKE/ILIKE operators.

Full Unicode case-insensitivity for ILIKE requires the ICU-enabled build of
@rocicorp/zero-sqlite3; until that ships, ILIKE remains ASCII-folded (no
regression) while LIKE case-sensitivity and escaping take effect immediately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

@arv is attempting to deploy a commit to the Rocicorp Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
replicache-docs Ready Ready Preview, Comment Jun 1, 2026 2:28pm
zbugs Ready Ready Preview Jun 1, 2026 2:28pm

Request Review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns zqlite (the SQLite replica) LIKE/ILIKE behavior with Postgres and the in-memory IVM matcher so query results are consistent across backends.

Changes:

  • Routes LIKE/NOT LIKE/ILIKE/NOT ILIKE through a dedicated SQL builder that adds ESCAPE '\' and implements ILIKE via lower(lhs) LIKE lower(rhs).
  • Enables PRAGMA case_sensitive_like = ON for each Database connection so bare LIKE becomes case-sensitive (Postgres-compatible).
  • Adds unit tests that pin the generated SQL for all four operators.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/zqlite/src/query-builder.ts Adds likeConditionToSQL() to generate Postgres-compatible LIKE/ILIKE SQL (including explicit ESCAPE).
packages/zqlite/src/query-builder.test.ts Adds assertions for the exact SQL emitted for LIKE/NOT LIKE/ILIKE/NOT ILIKE.
packages/zqlite/src/db.ts Sets case_sensitive_like pragma at connection creation time to match Postgres LIKE case-sensitivity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/zqlite/src/db.ts
arv and others added 2 commits June 1, 2026 16:11
Addresses review feedback: use the Database wrapper's pragma() method so the
call goes through #run() for consistent SqliteError context and slow-query
logging, matching the adjacent page_size pragma. Update the slow-query log
test to expect the extra (now-logged) construction-time pragma.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new `case_sensitive_like = ON` pragma in the zqlite Database constructor
(routed through Database.pragma()) advances the run-ast tests' mocked clock by
a fixed +2, shifting the snapshotted start/end timing values. elapsed is
unchanged. No behavior change — only the deterministic timing offsets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arv

arv commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@arv
arv added this pull request to the merge queue Jun 1, 2026
Merged via the queue into rocicorp:main with commit ff587d7 Jun 1, 2026
26 checks passed
@arv
arv deleted the arv/zqlite-unicode-like-ilike branch June 1, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants