Match modes
MUI’s four string operators (contains, notContains, startsWith, endsWith) are
case-insensitive by definition, and there is no portable way to write that in SQL.
TextMatch is the enum that picks the spelling; by default it is resolved from your
database driver.
Why this exists
The same operation is written three ways across three databases, and the difference is not cosmetic:
| Database | Case-insensitive match | What ILIKE does |
|---|---|---|
| PostgreSQL | col ILIKE :v |
works |
| MySQL / MariaDB | col LIKE :v (with a *_ci collation) |
syntax error |
| SQLite | col LIKE :v (ASCII case) |
syntax error |
So on MySQL and SQLite ILIKE is not “slower” or “less precise” — it is a broken query.
The spelling therefore cannot be baked into the library: it belongs to the connection, not to
the grid.
Values
| Case | SQL for contains |
For |
|---|---|---|
TextMatch::Auto |
resolved from the repository’s driver | the default |
TextMatch::ILike |
col ILIKE :v |
PostgreSQL |
TextMatch::Like |
col LIKE :v |
MySQL / SQLite — the collation decides the case |
TextMatch::Lower |
lower(col) LIKE lower(:v) |
any database, including non-ASCII folding |
The other nineteen operators are plain standard SQL; the mode does not touch them.
Automatic resolution
In Auto mode MuiGrid::wrap() resolves the spelling once per call, from the driver of the
database the given repository talks to:
| PDO driver | Mode |
|---|---|
pgsql |
ILike |
mysql |
Like |
sqlite |
Like |
| anything else | Lower |
The driver comes from the database config bound to the repository. By this point the config
is already registered with the connection pool and sits in its cache, so resolution costs one
static-array lookup rather than a new connection. If the config cannot be resolved — no pool,
an unregistered class, a test double — the portable Lower applies instead of an exception.
$schema = GridSchema::make(
GridColumn::for('title', 'a.title')->filterable(FilterType::String)->sortable(),
);
// on PostgreSQL → a.title ILIKE :iqb0
// on MySQL and SQLite → a.title LIKE :iqb0
// the code is identicalPinning it yourself
Detection knows the driver but not your collation. An explicit mode is needed in three
cases: a binary MySQL collation where LIKE is case-sensitive, an exotic driver, or
building a condition without a live connection pool.
For the whole schema
GridSchema::make(/* … */)->textMatch(TextMatch::Lower);For one column
GridColumn::for('name', 'au.name')
->filterable(FilterType::String)
->textMatch(TextMatch::Lower);A column’s mode overrides the schema’s. TextMatch::Auto on a column means “inherit from the
schema” — the same as not calling the method.
Methods
TextMatch::forDriver()
Returns the concrete mode for a PDO driver name.
Syntax
public static function forDriver(string $driver): selfParameters
$driver — the driver name, e.g. pgsql, mysql, sqlite. Case-insensitive.
Returns
TextMatch — a concrete mode; Lower for an unrecognised driver.
TextMatch::forRepository()
Returns the concrete mode for the database a repository talks to.
Useful beyond the grid: if you build your own search condition, the same call keeps it portable — see Filters outside the grid.
Syntax
public static function forRepository(RepositoryInterface $repository): selfParameters
$repository — the repository whose connection decides the dialect.
Returns
TextMatch — a concrete mode; Lower if the config could not be resolved.
Example
$m = TextMatch::forRepository($repo);
$repo->andWhere(Qb::clip(Qb::or(
$m->match('a.title', "%$s%"),
$m->match('a.body', "%$s%"),
)));TextMatch::match()
Builds a pattern-match condition in this mode.
Syntax
public function match(string $column, string $pattern, bool $negated = false): QbParameters
$column — a trusted SQL column expression. As everywhere, it comes from your code.
$pattern — the LIKE pattern including its wildcards: you place them, the method adds
nothing.
$negated — build the negated form (NOT LIKE / NOT ILIKE).
Returns
Qb — the condition with the value bound.
Errors
LogicException — when called on TextMatch::Auto. That case is an instruction, not a
spelling: it must be resolved before SQL is built.
Example
echo TextMatch::Lower->match('a.title', '%Winter%')->getQuery();Result
lower(a.title) LIKE :iqb0Notes on Lower
Lower folds both sides: the column with SQL lower(), the pattern with PHP’s
mb_strtolower(). Two things follow.
It is the only mode that fixes non-ASCII. In SQLite without ICU, LIKE ignores case for
Latin letters only: Ünïcode and ünïcode are different strings to it. Folding with
lower() solves that.
// the value that ends up bound
TextMatch::Like->match('a.title', '%WiNTeR Ünïcode%'); // '%WiNTeR Ünïcode%'
TextMatch::Lower->match('a.title', '%WiNTeR Ünïcode%'); // '%winter ünïcode%'Two lower() implementations can disagree. PHP’s folding and the database’s use
different case tables and diverge in rare locales. If that matters, pin ILike or Like
explicitly.
Indexes
ILIKE 'x%' never uses a btree index, while lower(col) LIKE 'x%' does — provided a
functional index on lower(col) exists. For contains the point is moot: a leading % is
unindexable on every database. More in
Dialects & indexes.
Next steps
- Operators — which operators exist at all.
- Dialects & indexes — why it works this way and what it means for performance.