Problem/Motivation

Drupal core's MySQL driver builds every upsert using the VALUES() function in the ON DUPLICATE KEY UPDATE clause. See https://git.drupalcode.org/project/drupal/-/blob/main/core/modules/mysql...

$update = [];
foreach ($insert_fields as $field) {
  $update[] = "$field = VALUES($field)";
}
$query .= ' ON DUPLICATE KEY UPDATE ' . implode(', ', $update);

MySQL deprecated this back in 8.0.20. Using VALUES() to refer to the new row throws warning 1287:

'VALUES function' is deprecated and will be removed in a future release. Please use an alias (INSERT INTO ... VALUES (...) AS alias) and replace VALUES(col) in the ON DUPLICATE KEY UPDATE clause with alias.col instead

The row-alias replacement has been available since MySQL 8.0.19 - see https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-20.html.

This is not a rare edge case. The cache backend leans heavily on Upsert, so every cache write emits one 1287 warning per non-key column being updated. On a busy site that scales directly with traffic and cache complexity, and it can completely dominate the warning counts you see in performance_schema (for example events_statements_summary_by_digest.SUM_WARNINGS and the events_errors_summary_* tables). Depending on log_error_verbosity and any query-logging or proxy layer sitting in front of the database, it can also add a lot of noise to logs, plus a small but non-zero per-statement cost to generate the warning each time.

Steps to reproduce

  1. Run Drupal on MySQL 8.0.20 or later (this does not apply to MariaDB — see below).
  2. Make warnings visible — query performance_schema, or put the connection into a mode where warnings are surfaced.
  3. Trigger any upsert. The easiest is a cache write, or call the API directly:
    \Drupal::database()
      ->upsert('cachetags')
      ->key('tag')
      ->fields(['tag', 'invalidations'])
      ->values(['node:1', 1])
      ->execute();
  4. Observe warning 1287 raised once per non-key column. The generated SQL is:
    INSERT INTO {cachetags} (tag, invalidations)
    VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1)
    ON DUPLICATE KEY UPDATE invalidations = VALUES(invalidations)

Proposed resolution

On MySQL 8.0.19+, use the row-alias syntax instead of VALUES(). The same query becomes:

INSERT INTO {cachetags} (tag, invalidations)
VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1) AS excluded
ON DUPLICATE KEY UPDATE invalidations = excluded.invalidations

The catch is that this syntax is MySQL-specific. MariaDB does not implement the AS alias row-alias form for ON DUPLICATE KEY UPDATE, and VALUES() is not deprecated there. Since the same mysql driver serves both engines, core cannot switch unconditionally — it has to branch:

  • MySQL ≥ 8.0.19 → emit the row-alias syntax.
  • MariaDB and older MySQL → keep the existing VALUES() syntax.

The driver already differentiates the two engines via Connection::isMariaDb(), so no new detection plumbing is required - only a version check on top.

An alias name needs picking that is unlikely to collide with a real column name. excluded reads nicely and mirrors the pgsql driver, which already uses EXCLUDED for the same concept, so there is some pleasing symmetry in the codebase.

Prior art: this exact problem has already been fixed in several other PHP data layers - Laravel gated it behind a use_upsert_alias config (see https://github.com/laravel/framework/pull/42053), and there are equivalent issues in Doctrine DBAL, GORM and SQLAlchemy.

Remaining tasks

  • Sign-off on the approach from a database maintainer.
  • MR against main branching on isMariaDb() + MySQL version.
  • Settle the alias name and confirm collision handling.
  • Test coverage: assert the generated SQL for both the MySQL and MariaDB paths, and ideally assert no 1287 warning is raised on MySQL 8.0.20+.
  • Update any existing tests that assert on the exact upsert SQL string.
  • Decide on backport

User interface changes

None.

Introduced terminology

None.

API changes

No change to the public Upsert API. The SQL string produced by Upsert::__toString() changes on MySQL 8.0.19+, so any contrib or test asserting on the literal generated SQL will need updating. Worth a change record to flag it.

Data model changes

None.

Release notes snippet

The MySQL driver now uses row and column aliases for INSERT ... ON DUPLICATE KEY UPDATE on MySQL 8.0.19 and later, avoiding the VALUES() function that MySQL deprecated in 8.0.20 (warning 1287). MariaDB and older MySQL versions continue to use the existing syntax, so there is no change in behaviour for those engines.

(created with the help of AI, but based on real customers having this issue)

Issue fork drupal-3604126

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

wiifm created an issue. See original summary.

wiifm’s picture

Issue summary: View changes

wiifm’s picture

MR created. AI assisted. More unit tests then new code. Choose to use an alias name similar to what Laravel used.

wiifm’s picture

Status: Active » Needs review
vladimiraus’s picture

Status: Needs review » Needs work

Left few comments in code.