[3.0] Case insensitive comparisons (part 1 of 2) — a {ci:} type for the query language - #9596
Open
albertlast wants to merge 1 commit into
Open
[3.0] Case insensitive comparisons (part 1 of 2) — a {ci:} type for the query language#9596albertlast wants to merge 1 commit into
albertlast wants to merge 1 commit into
Conversation
Whether a string comparison folds case is decided by the database engine:
MySQL folds it in the column's collation, PostgreSQL compares exactly. Callers
handled that themselves by reading Db::$db->case_sensitive and wrapping the
column in LOWER(), which put the decision at every call site and left it out
wherever somebody did not think to add it.
{ci:column} moves it into the query string, where the substitution layer
already lives. It expands to the bare column on MySQL and to LOWER(column) on
PostgreSQL. {ci_string:key} is the matching value type, for the places that
were folding the value in SQL rather than in PHP.
The column is named inline rather than through $db_values, so that a
comparison shows in the query text which column it folds. Only a column name,
optionally qualified by a table alias, is accepted.
Memberlist keeps its own LOWER() loop, because it folds expressions such as
COALESCE(group_name, '') as well as plain columns, and those are not what
{ci:} accepts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Whether a string comparison folds case is decided by the database engine. MySQL folds
it in the column's collation; PostgreSQL compares exactly. Today every caller that cares
has to know this, read
Db::$db->case_sensitive, and wrap the column inLOWER()itself:That puts the decision at each call site and defaults to the wrong answer on PostgreSQL
when somebody does not think to add it. It also fails quietly — the query returns fewer
rows rather than erroring, so nothing reaches
smf_log_errors. #9592, #9593 and #9594 areall that shape.
This moves the decision into the query string, where the substitution layer already lives.
{ci:column}expands tocolumnon MySQL andLOWER(column)on PostgreSQL.The column is named inline rather than through
$db_values, so a comparison shows inthe query text which column it folds. Only a column name, optionally qualified by a
table alias, is accepted.
{ci_string:key}is the matching value type, for the two places that were foldingthe value in SQL rather than in PHP.
Then converts the call sites that were already branching on
Db::$db->case_sensitive.No behaviour change. Each conversion expands to exactly what the ternary it replaces
produced, on both engines. Two things worth calling out:
User::addQueryCustomizationsForLoadType()still readscase_sensitivefor itsparameters, because an
{array_string:}list has no{ci:}of its own. Only theduplicated
WHEREclause goes away. An{array_ci_string:}could follow if it turnsout to be wanted anywhere else.
Memberlistkeeps its ownLOWER()loop. It folds expressions such asCOALESCE(group_name, {string:blank_string})as well as plain columns, and anexpression is not what
{ci:}accepts. Only its value side is converted.Not reachable from the unit suite:
replacement__callback()is protected and reachedthrough
quote(), which needs a live connection to escape with. Part 2 adds the testthat is possible without one — a guard over the source files, so that a new comparison
written the old way fails CI instead of being found years later.
Verified by hand on both engines from the Docker environment, and
composer lintandvendor/bin/phpunitare clean.Issues References (Fixes|Related|Closes)
case_sensitivebeing a per-engine constant rather than a property of the column