Problem/Motivation

After upgrading from Drupal core 11.3.13 to 11.4.1, database upserts that have worked for years on a MySQL system starting failing with duplicate key errors. Reverting back to 11.3.13 fixed the issue.

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1: INSERT INTO "example_table" ("name") VALUES (:db_insert_placeholder_0), (:db_insert_placeholder_1), (:db_insert_placeholder_2) ON DUPLICATE KEY UPDATE; Array
(
    [:db_insert_placeholder_0] => Example value 0
    [:db_insert_placeholder_1] => Example value 1
    [:db_insert_placeholder_2] => Example value 2
)

The code in question uses \Drupal\Core\Database\Database connection to do the following:

$upsert = $connection->upsert($table)
  ->values($data_item)
  ->fields($field_names)
  ->key($key)
  ->execute();

Issue fork drupal-3611653

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

john.oltman created an issue. See original summary.

john.oltman’s picture

Issue summary: View changes
john.oltman’s picture

Issue summary: View changes
john.oltman’s picture

Issue summary: View changes
john.oltman’s picture

Issue summary: View changes
john.oltman’s picture

Issue summary: View changes
john.oltman’s picture

Issue summary: View changes
cilefen’s picture

git log --oneline 11.3.13..11.4.1|grep -iE "(upsert|database)"
a3e92473065 revert: #3605262 Database write optimizations result in inconsistent data in data and revision tables
c8c3972c9f1 task: #3586760 Use composite key Upsert queries in core
c648adcfce9 perf: #3588385 Add a transaction around the database key value store setMultiple()
72095f8d09c fix: #3588702 Install mysqli in test 11.3 database fixture
529ca79cc1d perf: #3561960 Combine the database queries in ::loadFromSharedTables() and ::loadFromDedicatedTables()
77799e3eb8d task: #3581442 Replace usage of uniqid() in the Database system
2e8705a4bb1 feat: #2547493 Add support for unique / primary key constraints composed of multiple fields for Upsert queries
d344795bfed Revert "task: #3569127 Add new 11.3.x database dump fixtures, without modules deprecated for removal in 12.x"
3d860a3c532 task: #3569127 Add new 11.3.x database dump fixtures, without modules deprecated for removal in 12.x
06d89e94aeb task: #3569127 Add new 11.3.x database dump fixtures, without modules deprecated for removal in 12.x
d9f4bb9c658 fix: #2722307 Move translation based conditions into database query on revisions overview page
c840c458bc3 perf: #3564689 Combine multiple cardinality field loading into a single database query
1f71e2efe92 perf: #3493290 Try to reduce the number of database queries in MenuTreeStorage::rebuild()
cbb23a2aebd fix: #3561800 Using \Drupal\Core\Database\Query\Insert::from() on postgres on a table with a serial field can result in duplicate key error
1fe6aeaf13e task: #3554579 Optimize database writes when re-saving a pending revision as the default one
673fd32689e task: #3549727 Fix LongLineDeclaration in module Controller, Hook and Database directories
223992a21c0 perf: #3551308 Combine single cardinality fields into a single database query when loading entities

#2547493: Add support for unique / primary key constraints composed of multiple fields for Upsert queries and #3586760: Use composite key Upsert queries in core actually changed upsert, so I would start there.

john.oltman’s picture

Title: Database upsert fails after upgrade from 11.3 to 11.4 » Upsert fails after upgrade from 11.3 to 11.4
john.oltman’s picture

Issue summary: View changes
john.oltman’s picture

Thanks for the tip @cilefen, this seems like the most likely culprit:

https://git.drupalcode.org/project/drupal/-/commit/2e8705a4bb18750227d23...

Will revert locally and see if that fixes it.

mondrake’s picture

The error is SQL syntax, not duplicated key: nothing is specified after ON DUPLICATE KEY UPDATE in the SQL statement string. This happens if the 'key' column(s) correspond with the 'field' column(s). To prove that you may try to patch with #3604286: Upsert - Allow to customize the behavior of the update when the insert fails and use more Upsert queries in core, that adds a specific \LogicException('There must be at least one column to update in an upsert operation') error in that case.

john.oltman’s picture

Reverting #2547493: Add support for unique / primary key constraints composed of multiple fields for Upsert queries fixed it, although I got an exception afterwards Cannot unset offset of type array on array in /var/www/html/docroot/core/modules/mysql/src/Driver/Database/mysql/Upsert.php:31 which could be an artifact of my build and being in a hurry. Trying the patch from #3604286: Upsert - Allow to customize the behavior of the update when the insert fails and use more Upsert queries in core next after "reverting the revert".

And yes, both the update field and primary key is the name column in this particular upsert.

mondrake’s picture

#13 then using upsert in this case is nonsense, because the key columns, being duplicated, are not changing and there are not columns to update.

john.oltman’s picture

Thanks @mondrake, I just tried #3604286: Upsert - Allow to customize the behavior of the update when the insert fails and use more Upsert queries in core and I did indeed get the new logic error in that MR. Here are my thoughts on it:

The code is in a service in a custom module that services various requests and is passed various update fields and key fields. If the key field does not exist yet, then it should insert a record, otherwise in this particular case it becomes a "no change" update (but in 90% of the calls it services, the fields and key are not the same). It is not nonsense conceptually to have a service like this. And it has worked for years. Having the behavior change for this "key field = update field" case is an unexpected regression, in my view. I have not seen any documentation indicating this case is not allowed.

john.oltman’s picture

Following up ... with 11.4 as it exists now, or with the patch in #3604286: Upsert - Allow to customize the behavior of the update when the insert fails and use more Upsert queries in core, the workaround is to detect the "key field = update field" in the service, then do a select query to see if the key exists, and if not, then do an insert query (and do nothing if the key exists). But this is what the upsert call should be doing, and has done, for many years.

mondrake’s picture

It's in fact an insert that should keep going in case of IntegrityConstraintViolationException, then.

But if an upsert was abused for this, let's see if we can fix it not to break things. Maybe we just deprecate the degenerate case for now and throw the exception in a later major.

john.oltman’s picture

Thanks I will try that MR and report back.

mondrake’s picture

Status: Active » Needs review
john.oltman’s picture

Status: Needs review » Needs work

I tried the MR and although it avoids an exception or error, it does not behave the same as 11.3 for a case when some keys are new and some already exist. In 11.3, the ON DUPLICATE UPDATE key = VALUES(key) allows the insert to succeed for the new values, and the other values are updated in a "no change" update. In 11.4 with the MR, the INSERT fails without a fallback. You may wonder how 11.3 does this when line 31 tries to remove the key field from the ON DUPLICATE UPDATE clause:

https://git.drupalcode.org/project/drupal/-/blob/11.3.x/core/modules/mys...

I stepped in with a debugger, and line 31 does nothing because $insert_fields is a zero-based array. So it has been an accident of history that this did not happen before. In 11.4, the array_combine at line 21 changes the array from zero-based to string keys:

https://git.drupalcode.org/project/drupal/-/blob/11.4.x/core/modules/mys...

This allows lines 32-34 to remove the key fields from the ON DUPLICATE UPDATE clause.

My suggestion is to do a new MR that simply removes lines 32-34 (and possibly line 21), so as to be 100% BC with 11.3. And repeat this in the other drivers. Personally I do not see the need to ever deprecate this, as I do not see it as degenerate to have a way of making one database call to guarantee that a set of keys exists. I do understand that if the key is an auto-incrementing field, it will fail on SQL Server - that would have been true already though.

mondrake’s picture

Issue tags: +Needs tests

Ah but then it's a multi-value insert, i.e. in the same statement there is input for multiple rows to be inserted, and failure of one will fail the whole batch. We certainly need a test first of all, as coverage of that scenario is missing; and we need to see that it can work on all the dbs.

I still believe that using upsert in this case is not efficient, as it will force an useless additional update on all the rows where the key is already present. So directionally IMHO regardless of how we end up here, we should deprecate supporting the degenerate case.

mondrake’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests

luckily all dbs have syntax that covers this case natively

john.oltman’s picture

Status: Needs review » Reviewed & tested by the community

Excellent work @mondrake! This worked perfectly. I left one nit on the MR for an improved comment. Otherwise I think this is ready to go.

May I kindly suggest not deprecating this functionality, especially since it is supported in all drivers. For a schema with 40 tables, and 2 of the tables only have a single column, it seems arbitrary that those 2 tables cannot use the upsert command at some point in the future. Would create a ton of extra work for those of us in this boat.

mondrake’s picture

#24 yeah in the end if there's native support and with that we can avoid db roundtrips for multiple inserts in favor of a single SQL statement, we might just call this a supported case. Even if core is not using it at the moment (and that's why it got missed), now there's test coverage that would prevent regressions. So I retract the proposal to deprecate.

john.oltman’s picture

Sounds good, thank you again for the quick turn on getting this resolved

john.oltman’s picture

Issue summary: View changes
mondrake’s picture

Status: Reviewed & tested by the community » Needs work

Asked AI for a review, and it suggested adding a test for the composite key degenerate case, which I think is a good idea.

mondrake’s picture

Added testDegeneratedCompositeKeyUpsert. AI assisted in the making.

mondrake’s picture

Status: Needs work » Needs review
john.oltman’s picture

Status: Needs review » Needs work

New test makes sense and looks good. There are a couple spell check errors on "Upserting" and "Zaphod".

mondrake’s picture

Status: Needs work » Needs review
john.oltman’s picture

Possible to rerun the failed tests? I would do it myself but no permissions. I would feel better if the PGSQL test did not fail. Although I doubt the failure is related to this MR. Guessing a rerun of that one will solve it.

mondrake’s picture

all green now

john.oltman’s picture

Status: Needs review » Reviewed & tested by the community

Tests passed this time

mondrake’s picture

Small tuning of the comments. Leaving at RTBC.

  • catch committed b98f2f3a on 11.4.x
    fix: #3611653 Upsert fails after upgrade from 11.3 to 11.4
    
    By: john....

  • catch committed 220ee9fe on main
    fix: #3611653 Upsert fails after upgrade from 11.3 to 11.4
    
    By: john....
catch’s picture

Version: main » 11.4.x-dev
Status: Reviewed & tested by the community » Fixed

Following the discussion I was having similar reactions to @mondrake including this one:

yeah in the end if there's native support and with that we can avoid db roundtrips for multiple inserts in favor of a single SQL statement, we might just call this a supported case

The changes here aren't necessarily easy to follow but I don't think the resulting logic is particularly more difficult than it was before, and we'd need to detect the case to throw an exception, so seems OK to leave it supported as long as it's not causing problems for other cases.

Committed/pushed to main, cherry-picked to 11.x and 11.4.x, thanks!

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.

john.oltman’s picture

Thanks @catch. I think you missed the cherry pick to 11.x

  • catch committed f7f3f3dc on 11.x
    fix: #3611653 Upsert fails after upgrade from 11.3 to 11.4
    
    By: john....

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.