Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
There are several issues related to primary keys across the 3 database drivers:
- Dropping a (schema) field that is part of a composite primary key throws an SQL exception on MariaDB >10.2.8. When dropping a field named
id
, for example:
Key column 'id' does not exist in table
Even though the field clearly does exist. https://mariadb.com/kb/en/library/alter-table explains this in more detail.
- When dropping a (schema) field that is part of a composite primary key, MySQL and our SQLite driver implementation remove the field from the primary key but leave the reamining primary key in place. This can leave the table in an invalid state. PostgreSQL on the other hand automatically drops the primary key entirely, which is the safer behavior.
- When renaming a field that is part of the primary key via
Schema::changeField()
our SQLite driver does not update the primary key definition accordingly so that the rename fails. This is becauseSchema::mapKeyDefinition()
makes an incorrect assumption about the$mapping
variable; it looks up a value by key, but it must look up a key by value.
Proposed resolution
- See 2.
- When dropping a field on any of the by Drupal supported databases (MySQL, PostgreSQL and SQLite), check whether it is part of a composite primary key, and - if so - drop the primary key - and do not recreate it afterwards.
- Fix SQLite's
Schema::mapKeyDefinition()
so that renaming a primary key field works on SQLite.
Comment | File | Size | Author |
---|---|---|---|
#47 | 2974722-47.patch | 9.13 KB | tstoeckler |
#43 | 2974722-43--tests-only.patch | 6.43 KB | tstoeckler |
Comments
Comment #2
tstoecklerThis works for me.
To test this, you must be running MariaDB > 10.2.8.
I tested this myself with the patch over at #2938951-45: The primary key is not correctly re-created when updating the storage definition of an identifier field, i.e. (https://www.drupal.org/files/issues/2018-05-17/2938951-45.patch) and running
\Drupal\KernelTests\Core\Entity\EntitySchemaTest::testPrimaryKeyUpdate()
which is contained over there.It fails in HEAD (in fact it generates the failures described at #2841291-155: Fix NOT NULL handling in the entity storage and 'primary key' changes when updating the storage definition of an identifier field) but it passes with this patch applied.
Comment #3
alexpottWhat happens on other dbs to the primary key? Reading https://mariadb.com/kb/en/library/alter-table/#drop-column leaves we to suspect that we need to take care of re-creating the primary key without the column.
Comment #4
mondrakeWould be nice to do #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test first so to get a common method that introspects the primary keys across different db platforms.
Comment #5
tstoecklerActually the primary key is dropped entirely. This is tested by #2938951: The primary key is not correctly re-created when updating the storage definition of an identifier field. I just queued tests on PostgreSQL and SQLite there to make sure.
Comment #6
tstoecklerRe @mondrake: I will look into that issue, thanks for pointing me there. I will let committers decide if we should block this on that. This is a bug specific to the MySQL driver, where that is a fix/improvement across all drivers. So I can see arguments either way, and I personally don't feel strongly.
Comment #7
alexpottTesting on MySQL Server version: 5.7.19 Homebrew
Comment #8
tstoecklerAhh, that's very interesting, thanks for setting me straight! I was wrong in #5. In #2938951-40: The primary key is not correctly re-created when updating the storage definition of an identifier field @amateescu actually dropped some of the test coverage without me noticing. Will re-add that. So that makes this a "needs work".
Comment #9
tstoecklerOK, this seems to work for me with the updated patch over at #2938951: The primary key is not correctly re-created when updating the storage definition of an identifier field. This now re-creates the primary key after dropping the field.
Comment #10
tstoecklerPer the latest comments in #2938951: The primary key is not correctly re-created when updating the storage definition of an identifier field I think we should expand this issue's scope so that it covers all database drivers. Also making this officially postponed on #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test.
Also adding a related issue that would allow testing with MariaDB in DrupalCI which would be awesome.
Comment #11
tstoecklerAlright, so this pulls in the relevant Postgres fix from #2938951: The primary key is not correctly re-created when updating the storage definition of an identifier field (but adapted for #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test now).
It also adds some dedicated test coverage. I added this to the existing
testSchemaAddField()
so as not to add another test method, as the class is a bit chaotic anyway. That required to change a couple things in the existing code and also makes the code a tad less nice than it could be. If people don't like that we can also add a whole new test method instead.This should pass on all three database drivers. The tests-only patch should fail on Postgres (and would fail on MariaDB, if we had an environment for that).
Comment #15
tstoecklerNot sure why I didn't see those failures locally. But it seems making various types of text and blob fields a primary key is not a good idea. That's not at all related to this issue, though, just an artifact of re-using the pre-existing test method. So I now went ahead and added dedicated test methods for what we want to test here.
WIth that the test looks quite a lot nicer. In fact this even allowed me to use a data provider. That in turn revealed one problem with the test because I had previously used
$this->assertFalse(...)
and had to switch to$this->assertEquals(FALSE, ...)
. BecauseassertFalse()
does not check strict equality it was passing, even though the value returned was an empty array. So the test is now not only more readable but also more correct.Using a data provider in the test also required me to create the table with
'primary key' => []
and this revealed that this currently actually breaks in PostgreSQL. So I fixed this as well. Since it's such an easy and simple fix, I sincerely hope that we can fix this here and don't have to split that into a separate issue. Adding a note about this to the issue summary, though, which needed an update anyway since #10.Let's see where this leaves us. Again, unless I messed something up, the tests-only patch should fail on PostgreSQL and the actual patch should pass on all three drivers.
Comment #16
tstoecklerJust noticed that I forgot to update the code in MySQL's
dropField()
for #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test.Here's an interdiff for that, but not re-uploading a full set of patches until they come back in the expected way.
Comment #17
tstoecklerThis comment is incorrect now and a leftover from the previous iteration of the test.
However, as I stated in #2938951-53: The primary key is not correctly re-created when updating the storage definition of an identifier field I think the more correct behavior would actually be to drop the primary key entirely instead of recreating a possibly broken primary key. So here's an interdiff and a review-patch for that, not sending that for a test-run either, but it does pass on all three drivers locally. I would love some feedback on that, so marking "Needs subsystem maintainer review".
Comment #18
tstoecklerAhh, I rolled the "test-only" patch incorrectly above, because I of course need to include the code - and not just the tests - from #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test. Ugh. Let's see if this is better. Not re-uploading the full patch again, as there are still some open questions here. Will re-upload the latest full patch once this is RTBC.
Comment #20
daffie CreditAttribution: daffie commentedI have reviewed you for humans patch from comment #15. The patch looks good. I do have some remarks:
Can we use the method Schema-> findPrimaryKeyColumns() here?
I do not see why this change is necessary. Please explain. :)
Is it maybe better to calculate from the saved primary key what the new primary should be and test that to the actual primary key. Just an idea.
Can we add an extra parameter to the test method. It should be the expected primary key. The test method should not calculate it.
We are not testing the dropping of fields in this method.
I think that the readability improves if we remove the variable $field_spec and just put
['type' => 'int']
in its place.Can we replace these two lines with:
.
The variable $connection is never used.
Can we add:
@covers ::findPrimaryKeyColumns
Comment #21
tstoecklerThanks for the excellent review, great points! Do you have any opinion on #17?
Some feedback in detail:
Still waiting for the new tests-only patch to come back before doing another round of patches.
Comment #22
tstoecklerOops, status change was unintentional
Comment #23
daffie CreditAttribution: daffie commented@tstoeckler:
For #21.2 Can you add a comment to the patch.
For #21.3 My idea was that it would maybe better use the saved primary key and use it to calculate what the new paimary key should be.
Then after the column drop test if the primary key is the same as the expected primary key. If not then update the primary key.
Comment #24
daffie CreditAttribution: daffie commentedThe big question for me is what other developers expect when they use the functionality. My idea is that they will not expect that the primary key will be dropped. I think that we should be as helpful to them as we can be, therefor update the primary key.
We do not have a subsystem maintainer any more since @crell left.
Comment #25
tstoecklerRe #21.2: I could add something like: "Ignore an explicitly specified empty array as primary key." or something like that, but I'm not really sure that adds much value?! The similar code in e.g. the MySQL driver also does not have a comment.
Re #21.3: We already do use the saved primary key. Or maybe you mean something else by that than I? And after the column drop there will be no primary key, because it will have been dropped. Sorry, I still don't think I really understand what you are getting at, maybe you can explain what you mean with a code sample?
Comment #26
daffie CreditAttribution: daffie commentedThe requested code example:
I am not saying that my solution is better. It is just an idea.
Comment #27
daffie CreditAttribution: daffie commentedFor #21.2: After some thinking I come to the conclusion that if we want to solve correctly then we also need testing for it. And therefor it needs its own issue. The problem is that it is very subtle change. I know that I would easily mix it up. Sorry :(
Comment #28
tstoecklerRe #26: Hmm... to be honest, I like my version better, as it avoids a needless round-trip to the database. (Not that this makes a difference performance wise, I just think stating up-front what we know about the behavior of the database sever makes it more clear.) I'll let a third person be the judge before changing it.
Re #27: But we do have explicit test coverage of this! The test added here fails on PostgreSQL without that. And it is explicitly a test about primary keys upon table creation, so it's not "tested by accident". So I disagree on opening another issue. Again, maybe we can get another opinion on this or maybe you can clarify your reasoning a bit more.
Comment #29
tstoecklerAlright, here's an updated version to get the non-controversial stuff out of the way.
Unfortunately adding test coverage for
changeField()
uncovered two more bugs one for PostgreSQL one for SQLite. I think that really strengthens my case that it doesn't make sense to split this issue up. We do not commit bug fixed without test coverage. However, to provide proper test coverage across all drivers, fixing multiple things at once is necessary without introducing hacks or workarounds to the tests.What I would be fine with would be to disable certain parts of the test for certain database drivers and to fix those issues in separate issues that are then postponed on this one. That would keep the hack/workaround in a minimal way and would allow us to proceed here more or less undisturbed. What I - again - object to is blocking this issue on an ever-increasing list of bugs all across the three database drivers. This is already postponed on another issue and it is currently blocking a very important issue that is itself the base of numerous fixes in the entity storage system.
Comment #30
tstoecklerOops, pressed Save before actually uploading all the patches.
Comment #31
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThe patch looks great to me! I love the fact that we're normalizing this behavior across all db drivers :)
I would change these to only
@see URL
on the next comment line, without the "see ... for more information." part.We should mention that we're also testing
::addField
,::changeField
and::dropField
here.Comment #32
tstoecklerThanks @amateescu, will fix your comments in the next round. In the meantime: do you have any opinion on #17?
Comment #33
mondrakeMy 2 cents... sorry I did not go through the entire issue, comment based on the IS.
IMHO when you are dropping a field that is part of the primary key, most likely you are in a transient situation where you'll probably be doing more changes to the table (adding new fields for example) before you'll re-create the primary key itself. However, I do not think we can assume that in the atomic
dropField
method. Dropping and recreating the primary key at that point is more or less an assumption; the calling code should be in charge of identifying if a field is part of the pk, drop it, drop the field, do more stuff and eventually create the new pk. I believe the right thing to do indropField
would be to throw an exception if you are trying to drop a field that is part of the PK - and avoid the risk of ending up with a wrong pk. Do not know how badly this will break things, though :(Comment #34
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedI just read through the documentation linked in the patch and my opinion is that PostgreSQL's and MariaDB's >= 10.2.8 behavior makes sense: after dropping a field that is part of a composite primary key, we have no idea if the resulting primary key without that field is still correct for the table or not.
A few examples in core also support this conclusion. For example, dropping the langcode field from an entity revision data table makes the resulting primary key (id + revision_id) unusable. Same for all the other entity tables with composite primary keys.
So I think it's fair to leave it up to the "application"/caller to decide and act upon whether they want to re-instate the primary key with the remaining columns or not.
Comment #35
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedGreat to see that @mondrake has the same opinion :)
As for the choice between dropping the primary key automatically in
dropField()
(PostgreSQL's behavior) and raising an exception (MariaDB' >= 10.2.8 behavior), I'm afraid we don't have much choice because of the BC implications, so we can realistically only choose the first option and drop the PK automatically.Comment #36
tstoecklerOK, went ahead and updated the patch accordingly, then. It seems only @daffie is of a different opinion. Leaving the "Needs framework manager review" tag, though, to get some more opinions on this.
I added a couple of lines to the test to re-gain some part of the test coverage we were otherwise losing due to the automatically dropped primary key. Also fixed #31.
And lastly fixed the test failure in ##3. I'm not sure why I'm not seeing that utf8mb4 index length issue locally, but can't be bothered to figure that out right now. The test should hopefully be green, though.
Comment #38
daffie CreditAttribution: daffie commentedThe patch looks good to me.
I have one remark left. There is no testing for an empty primary key. In the issue summary we state that we will allow the creation of tables without a primary key. After which the patch is RTBC for me.
For the dropping of a combined primary key, the argument of:
I for me a great argument to change my position on the issue.
I am not is a position to manual test this patch with MariaDB's >= 10.2.8. It would be great if somebody else could do that.
Comment #39
tstoecklerThanks @daffie, that's great news!
You're right that the issue summary was not quite up to date, fixed that. The tests-only patch above proves that the PostgreSQL fixes are not strictly necessary here, so I split them into #2976493: Creating a table with an explicitly empty primary key is broken on PostgreSQL and #2976495: $fixnull behavior is broken on PostgreSQL's ::addField(). That should alleviate your concern in #38, I think. What the tests-only patch also proves is that the SQLite fix is necessary, adding a description of that to the issue summary now.
Not providing a patch for the testbot now, as #36 already shows the correct test-fail scenario, this just removes some hunks from PostgreSQL driver.
Comment #40
daffie CreditAttribution: daffie commentedThanks @tstoeckler: great work from you.
Now we have to wait on #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test.
Also we have to wait for somebody who can manual test this patch with MariaDB's >= 10.2.8.
For the rest is the patch RTBC for me.
Comment #41
daffie CreditAttribution: daffie commented#2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test has landed. Started the testbot for the for review patch.
Comment #42
daffie CreditAttribution: daffie commentedComment #43
tstoecklerHere we go. Rebased onto the 8.6.x and updated the new test for the now protected
findPrimaryKeyColumns()
method.Comment #45
daffie CreditAttribution: daffie commentedLooks good to me.
The test only patch show that there is a bug and the full patch fixes that bug.
The code for this patch comes from is parent issue: #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test
For me there is enough testing for the problem.
What is mis is for somebody to manual test this patch with MariaDB's >= 10.2.8.
For the rest is the patch RTBC for me.
Comment #46
alexpottLet's use a more descriptive name than
$method
as that will make the test more readable.There's no need to do in_array() followed by array_search(). We can do something like:
Also it would be great is there was documentation about what the mapping is. As far as I can see its a list that maps [new] => [old]. Where the keys are the new field names and the values are the old field names.
What's really strange is the code that creates the mapping..
Er... no... you've mapped the new field to the old field!
In fact it is almost tempting to fix the caller here to do what it says and do:
I.e. swap $field and $field_new.
Comment #47
tstoecklerThanks fore the review, this fixes #46.
Re:
I had tried that initially, but I don't think it's possible - or at least not easily possible - because
$mapping
is also passed further below into::alterTable()
. Looking into that you see that the values of$mapping
can in fact be arrays, so we can't just swap that. The array values are not used in::changeField()
but::alterTable()
is called from various other places. So I don't think we can do that change here.Comment #48
amateescu CreditAttribution: amateescu for Pfizer, Inc. commentedThe framework manager's review has been addressed, so removing that tag. The patch has also been manually tested by @tstoeckler on MariaDB >= 10.2.8.
Can't find anything else to point out, not even nitpicks :)
Comment #49
hchonovI was asked by @tstoeckler to execute the test
\Drupal\KernelTests\Core\Database\SchemaTest::testSchemaChangePrimaryKey()
on my system, which runs with MariaDB 10.3.7. I confirm that the test passes. Great work! +1 RTBC.Comment #50
alexpottCrediting reviewers whose reviews affected the patch.
Comment #51
alexpottCommitted 8faf0fa and pushed to 8.6.x. Thanks!
We can only do this in 8.6.x because #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test was an 8.6.x only change.
Comment #53
tacituseu CreditAttribution: tacituseu commentedThis introduced intermittent test failure on PostgreSQL 9.1
https://www.drupal.org/pift-ci-job/986641
https://www.drupal.org/pift-ci-job/991466
Comment #55
tacituseu CreditAttribution: tacituseu commentedFollow-up: #2982755: Random failure in SchemaTest::testSchemaChangePrimaryKey with order of composite primary key