Problem/Motivation
When using a Merge/upsert the primary key cannot be updated in sql server
Steps to reproduce
Drupal\KernelTests\Core\Database\UpsertTest::testUpsertWithKeywords
282Drupal\Core\Database\IntegrityConstraintViolationException: SQLSTATE[42000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot update identity column 'id'.: MERGE [test33275901select] AS tgt USING(VALUES (:db_upsert_placeholder_0, :db_upsert_placeholder_1), (:db_upsert_placeholder_2, :db_upsert_placeholder_3)) AS src ([id], [update]) ON tgt.[id]=src.[id] WHEN MATCHED THEN UPDATE SET [id]=src.[id], [update]=src.[update] WHEN NOT MATCHED THEN INSERT ([id], [update]) VALUES (src.[id], src.[update]);; Array
283(
284)
285/opt/drupal/modules/sqlsrv/src/Driver/Database/sqlsrv/ExceptionHandler.php:47
286/opt/drupal/modules/sqlsrv/src/Driver/Database/sqlsrv/Upsert.php:76
287/opt/drupal/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php:82
288/opt/drupal/vendor/phpunit/phpunit/src/Framework/TestResult.php:703
289Caused by
290PDOException: SQLSTATE[42000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot update identity column 'id'.
291/opt/drupal/core/lib/Drupal/Core/Database/StatementWrapper.php:145
292/opt/drupal/modules/sqlsrv/src/Driver/Database/sqlsrv/Upsert.php:72
293/opt/drupal/core/tests/Drupal/KernelTests/Core/Database/UpsertTest.php:82
294/opt/drupal/vendor/phpunit/phpunit/src/Framework/TestResult.php:703
Proposed resolution
Remove [id]=src.[id] from the update part of the statement.
Remaining tasks
Core support extends to 8.9, 9.2 and 9.3, so This fix will be applied to 3.1.x, 4.2.x and 4.3.x
User interface changes
API changes
Data model changes
Comment | File | Size | Author |
---|---|---|---|
#14 | 3245242-14.patch | 1.64 KB | daffie |
Issue fork sqlsrv-3245242
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
Comment #2
BeakerboyThis test goes back to 9.0, but should probably be considered a bug even in 8.x.
Comment #3
daffie CreditAttribution: daffie commentedOn my local machine the UpsertTest is now passing.
Comment #4
daffie CreditAttribution: daffie commentedComment #5
BeakerboyI'm surprised that this works. I thought that the "SET IDENTITY_INSERT {' . $this->table . '} ON;" had to be in the same statement as the actual query. This is why it is prepended to the query string in the Insert class. Is there any concern that the driver will close the socket connection between these calls and the database will be confused if identity_insert is still on, or if some other "malicious actor" could force an identity insert when they shouldn't be able to by timing their activity?
Secondly, SQL SERVER allows only 250 inserts in a batch, so if a user wants to insert or upsert more than that, it has to be broken up into a smaller number. The code as I checked it in sums up the number from all the individual queries and returns the total. I'm worried that using $affected_rows = $stmt->rowCount(); will just return the number from the last query. I'm guessing the core tests do not perform an upsert with many-many changes. If that's the case, we should add one to our test suite to ensure the count is correct when multiple batches are excecuted.
Comment #6
daffie CreditAttribution: daffie commentedWe can wrap the 3 statement together in a transaction, when it fails which is catched by an exception, we do a rollback. Just like we do in the PostgreSQL version of the Upsert class. See: https://git.drupalcode.org/project/drupal/-/blob/HEAD/core/lib/Drupal/Co....
Comment #7
BeakerboyComment #8
BeakerboyAfter transitioning the code to match the provided patch, I am seeing a new error:
3) Drupal\Tests\aggregator\Kernel\Migrate\MigrateAggregatorStubTest::testItemStub
Drupal\Core\Entity\EntityStorageException: SQLSTATE[25000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot roll back savepoint_1. No transaction or savepoint of that name was found.: ROLLBACK TRANSACTION savepoint_1; Array
(
)
Comment #9
BeakerboyI thinks it’s unrelated…Need to look at it some more.
Comment #10
BeakerboyWhen an upsert has to be split up Into multiple batches, so you think the “set identitiy_insert On” and then “off” should just happen once, or with each merge statement? Should the transaction cover the entire Upsert, or each batch?
Comment #11
BeakerboyI ran the current 4.3.x branch against MigrateAggregatorStubTest five times and it had two errors (out of 10 assertions). One was as above and one:
Drupal\Tests\aggregator\Kernel\Migrate\MigrateAggregatorStubTest::testFeedStub
170Drupal\Core\Entity\EntityStorageException: SQLSTATE[42000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Operation failed. The index entry of length 1820 bytes for the index 'aggregator_feed_field__url_idx' exceeds the maximum length of 1700 bytes for nonclustered indexes.: INSERT INTO [test96925307aggregator_feed] ([uuid], [langcode], [title], [url], [refresh], [checked], [queued], [link], [description], [image], [hash], [etag], [modified]) OUTPUT (Inserted.fid) VALUES
171(:db_insert0, :db_insert1, :db_insert2, :db_insert3, :db_insert4, :db_insert5, :db_insert6, :db_insert7, :db_insert8, :db_insert9, :db_insert10, :db_insert11, :db_insert12)
When I ran the 3245242-a-merge-statement branch five times (your code), the Test class failed as indicated above twice.
I should look more closely at this test.
Comment #12
BeakerboyFixing #3131438: Excessive index length failures in MigrateAggregatorStubTest took care of the MigrateAggregatorStubTest errors
Comment #14
daffie CreditAttribution: daffie commentedThe fix for the branch 4.2. With this fix it passes the test Drupal\KernelTests\Core\Database\UpsertTest.
Comment #15
BeakerboyComment #17
Beakerboy