Problem/Motivation

The PostgreSQL driver casts PHP booleans to integers before binding query arguments:

// The PDO PostgreSQL driver has a bug which doesn't type cast booleans
// correctly when parameters are bound using associative arrays.
// @see http://bugs.php.net/bug.php?id=48383
foreach ($args as &$value) {
  if (is_bool($value)) {
    $value = (int) $value;
  }
}

core/modules/pgsql/src/Driver/Database/pgsql/Connection.php, checked on 11.4.4.

There is no equivalent cast in the SQLite driver, the MySQL driver, or the shared database layer. grep -rn 'is_bool' core/modules/sqlite core/modules/mysql core/lib/Drupal/Core/Database returns a single unrelated hit in Database.php.

So the same API call behaves differently depending on the database:

  • PostgreSQL — the cast above turns FALSE into 0. Matches.
  • MySQL — binds as '', which MySQL coerces to 0. Matches.
  • SQLite — binds as '', and SQLite's type affinity does not coerce it. Matches nothing.

The failure is silent — it returns a wrong answer rather than an error, and only on one supported database. Any module writing $query->condition($boolean_field, FALSE), which reads as entirely natural, has this bug on SQLite today.

A concrete case, because the shape generalises: a contributed module's cron reaper found expired records with $query->condition('revoked', FALSE). On SQLite it matched nothing, so time-boxed privileges were never revoked — and nothing errored. It shipped that way for months and was found by reading the code, not by anyone noticing. MySQL and PostgreSQL hid it completely.

Steps to reproduce

PDO level, PHP 8.4, SQLite in memory:

$pdo->exec('CREATE TABLE grants (id INTEGER PRIMARY KEY, revoked INTEGER NOT NULL DEFAULT 0)');
$pdo->exec('INSERT INTO grants (id, revoked) VALUES (1, 0)');
// then, for each value: SELECT COUNT(*) FROM grants WHERE revoked = :v

Result:

  false (PHP bool)   -> 0 rows
  int 0              -> 1 rows
  string '0'         -> 1 rows
  empty string ''    -> 0 rows
  PHP false binds as typeof=text quote=''

That last line is the cause: PDO binds a PHP false as the empty string, and SQLite evaluates '' = 0 as false.

Proposed resolution

Move the cast out of the pgsql driver and into the shared layer, so every driver agrees rather than one driver compensating for a PDO quirk the others share.

Failing that, give the SQLite driver the same cast pgsql already has — though that leaves the next driver to rediscover it.

Remaining tasks

  • Confirm the preferred layer for the cast (shared vs per-driver).
  • Add test coverage asserting a boolean condition matches identically across all three drivers.
  • Check whether the entity query layer needs anything beyond the database-layer fix.

User interface changes

None.

API changes

None intended. Queries that already work on MySQL and PostgreSQL would begin working on SQLite; a query relying on the current SQLite behaviour would be relying on matching nothing.

Data model changes

None.

Release notes snippet

Passing a PHP boolean as a query condition value now matches consistently across the MySQL, PostgreSQL and SQLite drivers. Previously only the PostgreSQL driver cast booleans to integers, so a condition comparing a field to FALSE silently matched no rows on SQLite.

Comments

jmcerda created an issue. See original summary.

jmcerda’s picture

Issue summary: View changes
jmcerda’s picture

Issue summary: View changes
jmcerda’s picture

Status: Active » Closed (duplicate)

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.