Skip to content

Unicode-aware case-insensitive LIKE matching (accented/non-ASCII characters) #222

Description

@DutchJaFO

Background

SQLite's LIKE operator only case-folds ASCII by default — confirmed against SQLite's own docs during #216. Any accented Latin character (é/É), Cyrillic, Greek, etc. is matched case-sensitively unless the ICU extension is loaded. This is a property of SQLite's core C library itself, not of the .NET provider — the same limitation applies whether the project used Microsoft.Data.Sqlite (what Quotinator actually uses) or System.Data.SQLite.

This was found as a borderline case during #216's audit and deliberately not fixed there — #216 is scoped to id/enum/name-lookup case-insensitivity (LOWER(column) = LOWER(@param) exact matches), which is unaffected by this (SQLite's LOWER()/UPPER() SQL functions are also ASCII-only, but exact-match comparisons don't depend on LIKE's own folding behaviour the way a partial/contains match does). This issue is only about LIKE-based partial/contains matching — /quotes/search's q/field parameters, character/author/source fuzzy filters, and any future free-text search.

Decision at filing time: deferred, not urgent — Quotinator currently has no translations that would exercise non-ASCII partial-match search in a way that's known to be broken, so the practical impact today is low. Filed to the maintenance milestone rather than left undocumented.

Two possible fixes

  1. Load the SQLite ICU extension (native binary, SQLITE_ENABLE_ICU/libSqliteIcu) via SqliteConnection.LoadExtension(...) — confirmed present and documented on Microsoft.Data.Sqlite across 3.0–10.0 (Microsoft Learn). Gives ICU's full Unicode case-folding and collation support, but requires sourcing or building a compiled ICU extension binary for both linux/amd64 and linux/arm64 and bundling it into the Docker image — an ongoing multi-arch maintenance cost.
  2. Register a managed C# collation/function instead — SqliteConnection.CreateCollation(...) or CreateFunction(...) (both confirmed present on Microsoft.Data.Sqlite, no version gate relevant to Quotinator's 10.0.10). A C#-side case-fold (e.g. string.ToUpperInvariant() or CompareInfo-based comparison) run as a custom SQL function/collation, invoked from the relevant LIKE-adjacent WHERE clauses instead of relying on SQLite's native LIKE. No native binary, no multi-arch build cost — likely the lighter-weight option, at the cost of it only applying to queries explicitly rewritten to use it (not a blanket fix for every LIKE in the codebase).

Scope

  • Decide between the two approaches above (or confirm neither is worth the cost yet).
  • If proceeding: identify every LIKE-based query in Sql.cs (/quotes/search, character/author/source fuzzy filters — see the "Entity-scoped filter-parameter convention" and /quotes/search's explicit LIKE exemption in CLAUDE.md) and apply the chosen fix.
  • Add a live/smoke test with a genuine non-ASCII accented-character fixture (e.g. a quote with "café" or "Zürich") proving case-insensitive partial match works post-fix.
  • Update CLAUDE.md's String centralisation / LIKE documentation to record the resolution.

Out of scope

Activity

  1. added this to the v1.8.0 milestone on Jul 25, 2026
  2. added 5 commits that reference this issue on Jul 25, 2026
  3. DutchJaFO commented on Aug 12, 2026

    @DutchJaFO
    OwnerAuthor

    Shipped in v1.8.0 and confirmed still in place on current `main`.

    # Status Requirement Method Verification
    1 ✅ Raw SQLite `LIKE` is confirmed ASCII-only case-insensitive (canary) Unit test `SqliteQuoteServiceUnicodeSearchTests.RawSqliteLike_AccentedCharacters_IsCaseSensitive`
    2 ✅ `UNICODE_CONTAINS` is registered on every connection from `SqliteConnectionFactory`, including a second connection Unit test `SqliteQuoteServiceUnicodeSearchTests.UnicodeContains_RegisteredOnEveryConnection`
    3 ✅ `UNICODE_CONTAINS('café', 'CAFÉ')` returns true Unit test `SqliteQuoteServiceUnicodeSearchTests.UnicodeContains_MatchesAccentedCaseVariant`
    4 ✅ With the flag off (default), `Search`/fuzzy filters do not match accented case variants Unit test `SqliteQuoteServiceUnicodeSearchTests.*_FlagOff_DoesNotMatchAccentedCaseVariant` — 8 tests
    5 ✅ With the flag on, `Search`/fuzzy filters do match accented case variants Unit test `SqliteQuoteServiceUnicodeSearchTests.*_FlagOn_MatchesAccentedCaseVariant` — same 8 call paths
    6 ✅ Every existing ASCII search/filter behaviour is unchanged with the flag off Unit test `SqliteQuoteServiceSearchTests` + `SqliteQuoteServiceConversationTests` — 60 tests total, all passing
    7 ✅ `SqlQueryGuardTests.AssembledQueryCases` covers both flag states for all 8 sites Unit test Full solution passing
    8 ✅ `unicode_aware_search` option present, schema'd, and translated identically in both `addon/` and `addon-beta/` Live Confirmed matching
    9 ✅ T1: app starts in VS without error Live Confirmed by developer — clean startup stats, no errors
    10 ✅ T2: toggle proven against a real container in both states Live Run against `quotinator:local`: flag-off → `NoResults`; flag-on → `Ok` with the fixture returned
    11 ✅ README.md/CLAUDE.md documentation updated Live Confirmed

    Retroactive close — this shipped in v1.8.0 but was never formally closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions