whereBinary(): How to Run Case-Sensitive MySQL Queries in Laravel 13.27
Laravel Tips & Tricks #Laravel #MySQL #Query Builder #Laravel 13 #Eloquent

whereBinary(): How to Run Case-Sensitive MySQL Queries in Laravel 13.27

4 min read Mohamed Said Mohamed Said

The Problem with Laravel's Default MySQL Collation

MySQL string comparisons in Laravel run through the default utf8mb4_unicode_ci collation, which treats several differences as irrelevant: case, accents, trailing whitespace, and some Unicode normalization variants. That is exactly what you want for a display-name search, and exactly what you do not want for a token, a slug, or any value where the bytes themselves are the identity.

This query silently matches A7f3B9, a7f3b9, A7F3B9, and even A7f3B9 (note the trailing space):

DB::table('invites')->where('token', $request->token)->first();

The traditional escape hatch was whereRaw():

DB::table('invites')->whereRaw('token = BINARY ?', [$request->token])->first();

That works, but it steps outside the query builder, making the clause harder to compose with scopes, when() calls, and other conditions.

Introducing whereBinary() in Laravel 13.27

Laravel 13.27 ships four new query-builder methods that wrap the BINARY cast cleanly:

// Exact equality
DB::table('invites')->whereBinary('token', $request->token)->first();
// select * from `invites` where `token` = binary ?

// Negation
DB::table('users')->whereNotBinary('username', $username)->get();
// select * from `users` where `username` != binary ?

// OR variants
DB::table('users')
    ->where('id', $id)
    ->orWhereBinary('username', $username)
    ->get();
// select * from `users` where `id` = ? or `username` = binary ?

Values are still bound as parameters, so parameterization is identical to the whereRaw() approach. The difference is that the clause stays inside the builder and composes naturally with everything else.

Eloquent models work the same way:

$invite = Invite::query()
    ->whereBinary('token', $request->token)
    ->where('expires_at', '>', now())
    ->firstOrFail();

What "Binary" Actually Changes

The BINARY keyword casts the operand to a binary string, forcing a byte-for-byte comparison. Four categories of difference start mattering:

  • CaseAda no longer equals ada.
  • Accentsresume no longer matches résumé.
  • Trailing whitespace'ada' and 'ada ' are different byte lengths and no longer compare equal.
  • Unicode normalizationé as one code point versus e plus a combining accent are distinct byte sequences.

The trailing-whitespace and normalization cases are the ones most likely to cause silent bugs, because they produce false positives rather than obvious failures.

Engine Support and Test-Suite Implications

MySQL and MariaDB support whereBinary(). Every other engine throws:

RuntimeException: This database engine does not support binary comparison operations.

This is intentional. PostgreSQL and SQLite already compare strings case-sensitively by default, so a BINARY cast would either be a no-op or make a promise the driver cannot keep. If your test suite runs on SQLite but production runs on MySQL, any test covering a whereBinary() clause needs a MySQL-backed connection.

The Index Caveat — and How to Handle It

An index built on a _ci column cannot be used to satisfy a BINARY comparison, because the collations differ. On large tables, the recommended pattern is to let the index narrow the result set first, then apply the binary filter:

DB::table('invites')
    ->where('token', $request->token)        // uses the index
    ->whereBinary('token', $request->token)  // filters to byte-exact matches
    ->first();

The better long-term fix for columns that should always be compared byte-exactly is to declare the right collation in the migration:

$table->string('token')->collation('utf8mb4_bin')->unique();

This also solves something whereBinary() cannot: a unique index on a _ci column will reject Ada when ada already exists regardless of how you query. whereBinary() is a read-side tool; uniqueness enforcement is determined by the column's collation.

For pattern matching with wildcards, whereLike() already accepts a caseSensitive argument that compiles to like binary on MySQL:

DB::table('users')->whereLike('username', 'ada%', caseSensitive: true);

Use whereBinary() for equality checks and whereLike(..., caseSensitive: true) for pattern matching — the query planner has more optimization options for = than for LIKE.

Key Takeaways

  • whereBinary() was added in Laravel 13.27 alongside orWhereBinary(), whereNotBinary(), and orWhereNotBinary().
  • It replaces whereRaw('column = BINARY ?', [...]) while keeping the query fully composable.
  • Only MySQL and MariaDB are supported; other engines throw a RuntimeException.
  • Binary comparisons bypass column indexes — use the double-condition pattern on large tables or set the column collation to utf8mb4_bin.
  • whereBinary() is a read-side tool; unique constraint enforcement still depends on the column's collation.

Source: whereBinary(): Case-Sensitive MySQL Queries in Laravel — Laravel News

Found this useful?

Frequently Asked Questions

3 questions
Q01 Does whereBinary() work with Eloquent models, or only the DB facade?
It works with both. You can call whereBinary() on an Eloquent query builder just as you would on a DB::table() query, and it composes with other conditions like where(), when(), and query scopes.
Q02 Will whereBinary() use my existing index on the column?
Generally no. An index built under a _ci collation cannot satisfy a BINARY comparison. On large tables, chain a regular where() first to let the index narrow the rows, then add whereBinary() to filter to byte-exact matches. For columns that are always compared byte-exactly, setting the column collation to utf8mb4_bin in the migration is the better permanent fix.
Q03 What happens if I use whereBinary() with a SQLite or PostgreSQL test database?
Laravel throws a RuntimeException because those engines are not supported. If your test suite runs on SQLite but production uses MySQL, any test that exercises a whereBinary() clause needs to run against a MySQL-backed connection.

Continue reading

More Articles

View all