Search and table filters on SQLite ignore case only for ASCII: \"école\" does not find \"École\ #505
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#505
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the search and table filter lookups and running them against SQLite with the snapshot's interpreter.Related to #29.
Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atjobs/forms.py:384and:418(thename__iexactduplicate-name checks); the other locations have not moved.Global search and every text filter on a table use
__icontains. On SQLite, the default database, Django turns that intoLIKE, and SQLite'sLIKEfolds case for ASCII letters only. A query in lower case misses every record whose matching word has an accented or non-Latin capital, and the reverse. Postulo is translated into some sixty languages, most of which have such capitals.core/search.py:113-118,contains, andcore/search.py:130-144,ranked, used by every search group;core/tables.py:563-569,Table.filter, for everytextcolumn;config/sqlite.py, which sets pragmas and the transaction mode but registers no Unicode-aware function;config/settings/base.py:134-138, where SQLite is the default. The same applies to the other__icontains/__iexactlookups insrc/postulo(52 outside migrations), among them the duplicate-name checksname__iexactinjobs/forms.py:383and:417.école,škoda,KRAKÓWorαθηναϊκή, and finds them forÉCOLE,ŠKODA,kraków. The Companies Name filtername=écolereturns an empty table. The duplicate-name check lets škoda through beside Škoda.UPPERfollows the database locale), so the behaviour differs between the two supported databases.likewhen a connection is created, in aconnection_createdreceiver in core:connection.connection.create_function("like", 3, fn, deterministic=True), wherefn(pattern, value, escape)translates the pattern (%,_and the escape character) into a regular expression and matches it againstvaluewithre.IGNORECASE, or comparescasefold()of both sides. Django's SQLiteicontains,iexact,istartswithandiendswithall compile toLIKE … ESCAPE '\', so one function covers every lookup. Note the receiver inconfig/sqlite.py's docstring.Done when
tests/test_search.pyhas a test that searchingécole,škodaandαθηναϊκήfinds companies named École…, Škoda and Αθηναϊκή…, andtests/test_tables.pyone that the Companies name filter does the same; both fail on SQLite today.