Drupal currently does not make any use of foreign keys in the database (see #911352: Document that foreign keys may not be used by all drivers). DBTNG should provide methods for developers to create foreign keys if they want to use them.
Currently, foreign keys are defined in hook_schema() implementations like this:
'foreign keys' => [
'node_revision' => [
'table' => 'node_revision',
'columns' => [
'vid' => 'vid',
],
],
],
I suggest that a developer who wants the foreign key to be created would do it like this:
'foreign keys' => [
'node_revision' => [
'table' => 'node_revision',
'columns' => [
'vid' => 'vid',
],
'on update' => 'cascade',
'on delete' => 'set null',
],
],
If both 'on update' and 'on delete' are set to valid values, FKs would be created in the database, otherwise, they would not be. Valid values are: "restrict", "cascade", "set null", and "set default" ("no action" is valid in SQL but will not be supported for the reason given in #137).
Changes required for each database driver:
- createTableSql(), createKeysSql(), and _createKeys() methods need to create foreign keys, much like they create unique keys.
- addForeignKey(), dropForeignKey(), and foreignKeyExists() methods need to be added to the API.
Comment | File | Size | Author |
---|---|---|---|
#154 | drupal-foreign_key-2254131-154-D9.patch | 24.44 KB | Liam Morland |
#154 | interdiff-152-154.txt | 2.89 KB | Liam Morland |
#152 | drupal-foreign_key-2254131-152-D9.patch | 24.47 KB | Liam Morland |
#149 | drupal-foreign_key-2254131-149-D9.patch | 24.47 KB | Liam Morland |
#148 | drupal-foreign_key-2254131-148-D9.patch | 24.47 KB | Liam Morland |
Comments
Comment #1
Liam MorlandComment #2
davidwbarratt CreditAttribution: davidwbarratt commentedI support this!
Comment #3
Liam MorlandPatch for MySQL and Postgres.
Comment #4
Crell CreditAttribution: Crell commentedI'm a little skeptical. If you create actual foreign keys, much of Drupal's use of SQL breaks. The idea of adding a feature that we document that you can't use doesn't seem like a win...
Comment #5
Liam MorlandThis just making it possible for developers to use foreign keys if they want to. I don't think davidwbarratt and I are alone in wanting the ability to do this. I was recently chasing down a bug in a contrib module I work on that could not have happened if the module had used foreign keys. This is not about changing how core uses the database. This is just making a more complete database API.
Comment #6
davidwbarratt CreditAttribution: davidwbarratt commentedLiam,
Could you put this into a contrib module? Just create a new Database Driver and extend the existing class? That way Contrib modules that need foreign keys can require the new DB Driver? (or the module could gracefully disregard the foreign keys when that driver is not in use).
Might be a better route since I highly doubt any new features are going to be in Drupal 8.
Thanks!
Comment #7
Liam MorlandThat would make it much more difficult for modules to use foreign keys since they would have to depend on the proposed module. With the patches, anyone who wants to use foreign keys can just use them.
I don't think it should be a big deal to get this feature added since it is so little code and has no side-effects at all. This code does nothing at all until a contrib developer decides they want to use a foreign key in their project.
Comment #8
davidwbarratt CreditAttribution: davidwbarratt commentedLiam,
I suppose since it's backwards-compatible it could get released in a Drupal 8 minor (see #2135189: Proposal to manage the Drupal core release cycle).
I understand that it'd be better to be in core, but I think it'd be nice to have it in a usable, "safe" state (by being in a module) that can be used right now, rather than 1) have to patch core every time or 2) have to wait on core to commit the review/commit the patch. If module creators can demonstrate the effectiveness of using foreign keys in Drupal, I think there's a case for it to be in core, but I just don't think there's enough support for DB-level foreign keys. :(
Thanks!
Comment #9
Liam MorlandI don't understand why anyone would want to prevent developers from using foreign keys in their modules. Putting this in a contrib module makes it a much bigger hassle for the user of the module, who now has to install a dependency.
I want to be clear that this patch does not enable foreign keys in Drupal. The patch doesn't do anything by itself. The patch just makes the DBAL more complete, giving more options to developers of contrib modules.
The patch would be useful to developers who want it it and would have no impact at all on everyone else.
What is the downside to adding this to Drupal?
Comment #10
ar-jan CreditAttribution: ar-jan commentedIt looks like the question is whether Crell's concern in #4 is justified or not:
Say you implemented your own schema with foreign keys, would/could that break any of Drupal's funcionality?
Perhaps an example implementation that works without problems could move things further.
Comment #11
Liam MorlandNo. Even if someone configured a table in their contrib module to have a FK relationship to a core table, changes in that core table would cascade to the module's table. This patch has no effect at all unless a developer specifically configures FKs to be created and even then, the FKs only effect how the contrib module's own tables behave.
I'm not sure what you mean by an "example implementation". Do you mean a contrib module that uses FKs? That would be a bit of a chicken-and-egg problem since it is a problem to use FKs if the DB abstraction layer doesn't have a means to create them.
Comment #12
ar-jan CreditAttribution: ar-jan commentedRight, it's clear that nothing would happen if you don't implement FKs. I take Crell's comment to mean that you could break your own module, or interaction between your module and Drupal core, by implementing FKs.
So yes, I was thinking of D8 + your patch and a contrib demo module to show some things that wouldn't break, as an example. Though I suppose that could be a lot of work.
Comment #13
Liam MorlandI don't have any D8 modules yet. I want this for D7, but I understand that features like this need to be added to D8 before they could be added to D7 (and at this point, probably won't be added to D7). I don't really understand what such a module would demonstrate. Of course adding FKs within a module won't have any side-effects outside of out, and if the FKs are set to cascade, they won't prevent any other database operations from happening either.
Of course, any developer can break their own module, FKs or not. The use of foreign keys has many times save me from breaking my site and their absence in Drupal modules that I have worked on has caused me grief. From my perspective, the benefits are clear and I have not seen any explanation of how allowing developers to use this tool could be harmful.
Comment #14
Crell CreditAttribution: Crell commentedThe problem is situations like this:
When you delete a node, Drupal first deletes the record from the {node} table. It then lets contrib modules know (via hook) "by the way, that node has been deleted; you may want to clean stuff up", which then delete whatever records they have in tables that reference the now-deleted node. Fields do the same, just with a single module doing all of it.
If the nid column in the contrib module's table has a FK defined to the nid column of the {node} table, and you delete the record in the {node} table, SQL will (correctly) yell at you for a foreign key violation.
Now, in Drupal 8, ironically, I think this is less of an issue for the simple reason that you should not be writing SQL in Drupal 8. Really. Your module's data should be stored using the Config System, State System, or Entity System. There are extremely few cases where a non-site-specific module should ever be writing SQL anymore, which makes most of this issue moot.
Comment #15
davidwbarratt CreditAttribution: davidwbarratt commentedCrell,
In Drupal 8, which API would you use to do something like Comment Tree? It seems overkill that the comment_tree table would be an Entity, but perhaps that's my Drupal 7 concept of Entities, not the Drupal 8 thinking. Would every table in Drupal 8 be an Entity? If this is the case then it might be wise to consider having Foreign Key constrains for Entities rather than for the schema. Would it be possible to have database-level foreign keys (like the Doctrine ORM) on a per-entity basis? or would it be an all or nothing proposition?
Thanks!
Comment #16
Crell CreditAttribution: Crell commentedThat looks like an SQL-specific optimization tool that may not be relevant on other backends. (Say, if you're storing comments in MongoDB, for instance.) That would mean it qualifies as one of the "extremely few cases" (not zero cases) where custom SQL makes sense. In the majority case, however, people should be using EFQv2 for all entity access.
The problem with FKs for entities is that we'd need to completely reverse the order in which things happen when updating or removing an entity. The Entity API team has enough to do yet before we can ship. :-) If we had FKs then we could, arguably, rely on those for deletion. (Delete just the record from the node table and let the DB handle the rest) That may have who knows what other side effects. (Removing log entries? Does everything that references a node really get deleted when the node does?) It would also mean we couldn't use a non-FK-enforcing database table; that really means just MyISAM, which there's work to restore happening right now. :-) (Not that I'd mind losing MyISAM myself, but there are larger implications.)
My overall point is that the use case for this change in D8 is fairly small, and the impact of trying to leverage it quite large. That feels like a bad trade-off to me, especially when we really really really want a beta out soon.
Comment #17
davidwbarratt CreditAttribution: davidwbarratt commentedCrell,
Thanks for taking the time to answer that for me. I know you're a busy guy. :)
I think the point of having foreign key constraints, is not to use CASCADE, but rather to have better data integrity. I know that's a big issue and is not solved by simply having FKs, but I think it's an important part of it. Ironically, I would think that Enterprises would care about database-level data integrity, if this really is something that is important to the Enterprise, I would imagine that it's also important to Drupal.
I actually hate using CASCADE for the very problems you mention. It takes control of deleting out of the hands of the program and into the database which may or may not support them (and if it it does, how do you know the user hasn't disabled them?).
Another way of solving this problem is by allowing NULL on fields with database level foreign keys. This way, when data that is related to a node (our example) is deleted, Drupal should first set the FK fields to NULL, then delete the node, then it just needs to clean up any NULL relations. This way the order in which Drupal delets things does not change, the only thing that changes is what Drupal looks for when deleting. It might be weird to have NULLs in other tables (like field tables), but technically this is accurate. The field can either be a legitimate nid or it can be null, it can't be anything else. If you were to delete a node, it should be NULL since the relationship technically no longer exists. Right now, when Drupal deletes a node, it has relationships that are not valid in the database. This is the essence of the problem that should be remedied. You cannot rely on the Drupal database for valid relationships.
As far as your log question, I think that's an edge case, but in that scenario, I simply wouldn't have the constraints on the log table. In that scenario we're saying "It's ok if this relationship is no longer valid, and the relationship should not be relied upon." Right now however, we are saying that is the case with every table in Drupal. :(
Do you think it would be possible to create a module that replaces the standard EFQ in Drupal 8 with one that has FK support? (that way people who want it can at least have it even if Drupal doesn't plan on supporting them?)
Thanks!
Comment #18
Liam MorlandThe way my patch is written, it would not yell at you. Instead, the delete would cascade to the contrib module's table, so Drupal's functionality would be unchanged.
A site-specific module is a good example of where I would want to use this. We use databases that will not be moved into Drupal entities because the databases are used outside of Drupal as well. We want to use Drupal to manipulate these tables and we need to use the foreign keys.
It certainly is important to me. I have had to deal with Drupal sites that were badly broken in ways that could not happen if FKs existed. As I mentioned about, we have data that we will not risk putting in the hands of any system that does not enforce FKs.
This could be done automatically with ON DELETE SET NULL. Perhaps instead of create => TRUE, it should be configurable to either cascade or set null.
I don't see how much patch would hold up a beta. It just adds a small feature to the DBAL. There is not need to retrofit all of Drupal to use FKs. I just want FKs to be available to contrib developers who want to use them.
Comment #19
Crell CreditAttribution: Crell commentedAllowing either cascade or set-null would be a hard requirement for us adding this functionality.
Without cascade, though, what data integrity are you getting? Just the "in array" type validation that you don't write a nid that doesn't exist (but can orphan it later)?
I totally get that there are use cases where experts working with their own tables (eg, Liam in #18) would know how to leverage FK relationships properly in intelligent ways. My concern is the unwitting contrib author saying "Oh, I should totally create these, yeah!" without thinking through what the implications are (like, is the site even using SQL for that functionality, or is it using MongoDB or Redis?). That could cause *more* integrity issues on a site using that module than it saves. We would need to figure out how to properly communicate when it's appropriate to use. (And just throwing "Advanced dangerous stuff, beware!" on the key name, as we've occasionally done before, is not going to fly.)
Comment #20
Liam MorlandHow about this: Instead of setting "create" => TRUE, the developer needs to set an "on update" and "on delete" property for each FK. The values would be things like "cascade" and "set null", mirroring what one would have to write in an SQL CREATE TABLE statement. Both properties would need to be present before an FK would be created. This means that the developer needs to understand what they are doing; they can't just set "create" for the fun of it. Does that help?
Comment #21
Liam MorlandFor example, one might configure a FK like this:
Comment #22
Crell CreditAttribution: Crell commentedI discussed this issue with Alex Pott at the DrupalCon code sprint. We're OK with adding #20/#21, with the following caveats:
1) As noted above, if you don't specify the "on" behavior the key is not actually created in the database.
2) It would need full test coverage (of course). The tests need to also validate against Postgres and SQLite, at least for as long as those remain in core.
3) Nothing in core would make use of this functionality. It would be purely for custom site-specific modules. Contrib modules that rely on it and break something are SOL.
4) It needs to be in before RC1. If it misses RC1 it would not get into 8.0 as it's a feature addition. However, it's a non-breaking addition so it *would* be eligible for 8.1. So it's not a "now or wait 5 years" type urgency. :-)
Liam, David: Back over to you.
Comment #23
Liam MorlandThanks very much, Crell. I agree with the requirements. I will work on this.
Comment #24
Liam MorlandComment #25
xjmThis issue introduces a new feature, so per https://www.drupal.org/core/beta-changes (which was adopted several months after #23), we should postpone it to 8.1.x or later. Thanks all.
Comment #26
Liam MorlandUpdate issue summary to match current state of this initiative.
Comment #27
Liam MorlandAdded list of valid values for on update/delete.
Comment #31
Liam MorlandThe attached patch implements foreign keys and tests.
Comment #33
Liam MorlandComment #35
Liam MorlandComment #36
Pavan B S CreditAttribution: Pavan B S at Valuebound commentedLine exceeding 80 characters
Line exceeding 80 characters
Line exceeding 80 characters
Comment #37
Pavan B S CreditAttribution: Pavan B S at Valuebound commentedLine exceeding 80 characters
Line exceeding 80 characters
Line exceeding 80 characters
Comment #38
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedComment #40
jofitz CreditAttribution: jofitz at ComputerMinds commentedRe-rolled.
EDIT: Unable to create interdiff due to re-roll (as is often the case):
Comment #41
colan#40: Please provide an interdiff to illustrate any differences when providing updated patches.
Comment #42
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedAll the minor fixes have been resolved.
Comment #44
daffie CreditAttribution: daffie commentedFor the array's use the short notation
[]
and not the long versionarray()
.Comment #45
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedComment #46
gaurav.kapoor CreditAttribution: gaurav.kapoor at OpenSense Labs commentedComment #47
jofitz CreditAttribution: jofitz at ComputerMinds commentedRe-rolled.
Unable to create interdiff due to complexity of re-roll:
Comment #48
lostcarpark CreditAttribution: lostcarpark commentedI'm glad to see this making its way into core. Hopefully it will be included in 8.4, and I can spend my Christmas break adding foreign keys to my module tables!
Comment #49
Liam MorlandThanks. Could you review the patch?
Comment #51
Liam MorlandRe-roll of #47 for Drupal 8.5.x.
Comment #52
Peter MajmeskuIs this just for database table schemas or also for entities?
How do we handle migration/update of existing databases without foreign keys?
Comment #53
Liam MorlandThis just allows developers to add foreign keys to their tables if they want them. Nothing will change for any module unless it explicitly requests the creation of the FKs in the database. There is no migration since current data stays as it is.
Comment #54
Peter MajmeskuDoes this work for content/config entities or just for table schemas (= hook_schema() definitions)?
Comment #55
Liam MorlandThis patch only affects database tables.
Comment #56
Peter MajmeskuWhy not extend it, to work with entities also?
Comment #57
Liam MorlandI don't know what that would mean. Make a child ticket. This ticket has a narrow focus on just this one feature.
Comment #58
geek-merlinPatch #51: Nice work! Code looks straightforward.
One nit though:
I'd write that out like everything else.
Comment #59
Liam MorlandRe-roll with rename of fkExists() to foreignKeyExists().
Comment #61
Liam MorlandComment #62
Peter MajmeskuThe patch is failing the SQLite and PostgreSQL tests. Therefor it needs work.
If the patch is not intended to work with PostgreSQL and SQLite, then it should at least not affect the tests.
Comment #63
Liam MorlandYes, I set it to "needs review" after the MySQL test passed having initially failed for unrelated reasons. Then I added the other database tests. The PG one also appears to have failed for reasons unrelated to the patch.
Comment #64
Peter MajmeskuCan you prove the same failing tests, when you are testing without any code changes? That would prove your assumption.
Comment #65
Liam MorlandThis patch should work with SQLite (all FK-related functions return NULL). The Postgres error messages are out-of-memory errors. It is not obvious to me why these changes would cause that.
Comment #66
Liam MorlandComment #67
Liam MorlandThat failure I think I can fix. Stand by.
Comment #68
Peter Majmesku@Liam Morland: Thanks for the patch!
Which steps do I need to follow for proving that your patch works on my end? It would be nice, if you could post a "step-by-step instruction" of basically what you did for testing. Afterwards I can set the status to "Reviewed and tested by the community".
Comment #69
Liam MorlandComment #70
Liam MorlandComment #71
Liam MorlandClearly, Postgres needs a closer look. I will likely have time on Friday.
Update: Postgres is now passing. The errors were related to out-of-memory. It may be an intermittent problem with the testing environment.
Comment #72
Liam MorlandComment #73
Peter MajmeskuCan you post me the instruction for testing your patch?
Comment #74
Liam MorlandThe patch has build-in tests. It could also be manually tested to check that the three new methods work and that FKs are created at table creation time.
Comment #75
Peter MajmeskuCan I also create a custom entity with foreign keys on it's relation?
Comment #76
Liam MorlandNo, this ticket is only about the database abstraction layer's ability to manage database FKs.
Comment #77
Peter MajmeskuSchemaTest::testSchema() is a huge method. In my opinion it is done wrong anyways. There should not be such a huge function. The test methods should be more granular. May there is a better place for it. But we can keep it as it is for now in my opinion and refactor this in a different issue.
PostgreSQL: May it is an option to implement foreign keys for MySQL only? You have the option to check it via
I can confirm that the foreign key creation works.
Even if this issue is "not" about having foreign keys in entities, it is a pity that this code does not support entities. Just creating a DB schema seems so old nowadays. We should support foreign keys in entity as a community. Like Doctrine and any other ORM. I would suggest that we create an new issue, after the patch is being commited into core to enhance entities with foreign keys also. Can your code be re-used for entities in any way?
Comment #78
Peter MajmeskuI saw that the PostgreSQL are also passing now without any needed changes in this issue. It seems that the PostgreSQL tests do not have anything to do with this issue (PHP 7 & PostgreSQL 9.1 23,064 pass). Therefor I am changing the status to RTBC.
Comment #79
Peter MajmeskuI have created an issue for refactoring SchemaTest::testSchema() at https://www.drupal.org/project/drupal/issues/2935506.
Comment #80
Liam MorlandThe patch has passed with PHP 5.5 and PHP 7 on all three supported databases.
Comment #81
daffie CreditAttribution: daffie commented@Liam Morland: The idea of adding the functionality for creating and dropping foreign keys is great. I did a review of your patch and I do have remarks:
Can we make the return value boolean only. If the database does not support foreign keys then the foreign does not exist and should return the null value.
Can we have some more documentation what these options are and what they do. Are they required values? For both
on update
andon delete
? It is not clear to me how this works.Can the return value be boolean only. If the database does not support foreign keys then the foreign key could not be created and the return value should be false.
Can the return value be boolean only. If the database does not support foreign keys then the foreign key does not exist and there for could not be dropped, then the return value should be false.
This can be rewritten to:
We could add an extra test to see if the foreign key was really dropped.
Can there be some documentation added why the "CASCADE" keyword needs to be added.
For testing purposes it is necessary to keep the testing environment as close as possible to real world usages of Drupal.
My suggestion would be to create tests with and without these settings.
Comment #82
Liam MorlandThanks @daffie. I will have time on Friday to work on this.
Regarding 2, I will edit hook_schema() to include such documentation.
Regarding 7, these are just tear-down and clean methods. The changes ensure that they will work even if FK relationships still exist between tables.
Regarding 8, the other keys are tested in that test. It is more efficient to do it together. Otherwise it is a lot more created, changing, and dropping tables. I don't mind doing it separated, but I would be more comfortable seeing more traction on #2935506: Refactor SchemaTest::testSchema() to make it more granular before starting down that path. I've just been following how things are currently being done.
Comment #83
daffie CreditAttribution: daffie commentedMost Drupal sites will run with the default MySQL settings. For "foreign_key_checks" the default setting is "1". The testing in your patch is done with the setting "0". For adding this functionality to Drupal Core is needs to work with the default setting.
The SchemaTest::testSchema() is a very old test and it is not written in the way that we now would like it. Please make new tests for the foreign key functionality. If there is a problem with a test it is much easier to find out where the problem is. With tests like SchemaTest::testSchema() that is a lot harder.
Comment #84
Liam MorlandThe tests run with foreign_key_checks set to 1. It is only set to 0 for tear-down and clean-up. Without that, a failed test or a crash during testing could leave the database in a stat that the clean-up function cannot clean up. This is only needed for MySQL because it doesn't have a transactional DDL.
Comment #85
daffie CreditAttribution: daffie commentedYou are right.
Comment #86
Liam MorlandWhat is the advantage to always returning FALSE instead of NULL? FALSE suggests that something went wrong. Using NULL means one can easily tell that there isn't an error, it just isn't supported. In terms of eventually adding type hints to these functions, it is compatible with using the nullable types notation.
Comment #87
daffie CreditAttribution: daffie commentedSomehow it feels a bit wrong for me. Give me some time to think it over.
If you want to know if the database support foreign keys, should we not just add a method to the Connection class. Something like "Connection::supportForeignKeys()". For MySQL and PostgreSQL it will returns true and for SQLite it will return false.
Comment #88
Liam MorlandComment #91
Liam MorlandSorry about that. The interdiff above is the correct file. The differences are all in the comments, so testing results should be the same.
Comment #92
Liam MorlandComment #93
Liam MorlandThis patch is identical to #91.
Comment #94
Peter MajmeskuLooks good. How about commiting it into core?
Comment #95
tstoecklerThe test suite should be green on all three database backends before this can go in, so I don't think we're there yet.
I can't really provide much help here as this issue is a bit above my paygrade, but it certainly is pretty encouraging that the tests pass on MySQL without too much heavy-lifting so far. So would be nice to make progress on this.
Comment #96
Peter MajmeskuAre the failing tests related to this patch? I saw them passing.
Comment #97
Liam MorlandEverything passes now. The failures did not appear to be related to the changes.
Comment #98
Liam MorlandComment #99
Peter MajmeskuLooks good to me! Thank you for your work Liam Morland. Much appreciated.
Comment #100
larowlannit: I would prefer we named this $foreign_key for clarity
If the signature here was
I think it would make more sense, having a largely optional parameter first feels skewif
Same story on the drop
extra space here.
in_array should use the third argument in both instances, we're checking strings here.
https://3v4l.org/DWjeK
in my opinion this would be far more readable if it used
sprintf
We have this code in multiple places - would it make sense as a protected method on the base class?
this feels like something that would come back to bite us later. Are we really sure that silently wiping dependent tables is the right thing to do?
So we're defaulting to 'not supported'?
Perhaps we should say 'NULL if the database driver' instead of 'database' because for implementations in contrib, the database itself may support it, but the driver may not (until it is updated to reflect this patch)
We need tests for these code paths
we can probably inline this
Should we check the return of this before we return TRUE?
Same, should we check the return of ->query before returning TRUE?
can we use foreign key here instead of fk, for clarity sake
special casing mysql feels wrong, what if other contrib drivers are in the same boat?
sounds like we're missing a
supportsTransactionalDataDefinitionStatements
method or similarThere are no tests of invalid values for 'on update' and 'on delete'
Comment #101
Liam MorlandThanks very much for the review.
Re. #2, $table is not optional in Postgres. This would create compatibility issues if people get in the habit of leaving it out. As well, I was following the pattern of the other methods, which have table as the first parameter.
Re. #4, I don't mind using sprintf(). No other methods use it. I was just following existing practise.
Re. #9,
this cannot be inlined because the variable is used again later.It can be inlined; I misread the suggestion.Re. #10 & 11, if it fails, I thought it would throw an exception anyway.
I am working on points 6, 8, 13, and 14. The rest are addressed in the attached patch.
Comment #102
Liam MorlandThis new patch addresses #8, #9, #13, and #14 from comment #100. This means all issues raised by comment #100 are addressed by the patch or by comment #101, except #6, which I address below.
Re. #6, CASCADE is used so that tables can be easily deleted even if foreign key relationships exist between them. This is needed in clean-up functions which delete all tables in no particular order. On MySQL, this is accomplished by turning off foreign keys using toggleForeignKeyChecks(). On Postgres, we need CASCADE.
Outside of testing, the only time CASCADE would do anything is when a referenced table is deleted. The only tables it would impact are those that have signed-up for this by adding foreign keys. In normal operation, table are not being create and deleted. This typically happens when modules are installed or uninstalled. For example, webform_validation depends on webform. webform_validation could create foreign keys referring to webform tables. If those table were deleted, the data in the webform_validation table would no longer make any sense and should be removed anyway, so CASCADE is fine.
The current patch adds a parameter to dropTable() for requesting that CASCADE be included in the query. This parameter is used in the testing clean-up functions. To avoid changing the function signature, I used func_get_args().
Another possibility would be to check for the existence of foreign keys that reference the table in dropTable() and remove those keys prior to dropping the table. This could leave the database with tables that are intended to reference other tables, but now just don't.
I'm interested to hear others' thoughts about what to do here.
Comment #103
Liam MorlandI fixed a small bug in #102.
This is now failing due to out-of-memory. I don't know what I can do about that.I've noticed this before: Postgres testing is inconsistent. I don't think it has anything to do with my patch. It's passing now.Comment #104
Liam MorlandI was mistaken about the impact of DROP TABLE ... CASCADE. Only the listed table is dropped. The only thing CASCADE does is remove the foreign key relationship from other tables that reference it. This means that using CASCADE causes DROP TABLE to always work the way it currently does so we are safe to use it.
To get similar behavior in MySQL would require using
SET foreign_key_checks
to disable FK checking during every DROP TABLE. I think that would be fine.Comment #105
Liam MorlandThis patch uses DROP TABLE ... CASCADE or disabling FK checks during table drop (MySQL) so that dropping tables always succeeds, just like it does now. CASCADE only removes the FK reference; it does not cause the referencing table to be deleted. This addresses all issues raised in comment #100.
Comment #106
Liam MorlandA small improvement on the last one, making use of validReferenceAction().
Comment #108
Liam MorlandReroll.
Comment #110
Liam MorlandRemove deprecated db_create_table().
Comment #111
jofitz CreditAttribution: jofitz commentedReplace deprecated code.
Comment #112
Peter MajmeskuLGTM
Comment #114
Liam MorlandPostgres tests are failing due to out-of-memory errors. The tests used to pass with the patch. This suggests that the Postgres environment is right on the edge of having enough memory and the foreign keys just push it over the edge. I have reported this issue in #3011791: Error "out of shared memory" in Postgres testing.
Comment #116
Liam MorlandReroll.
Comment #118
Liam MorlandComment #119
Liam MorlandSwitch to short array syntax.
Comment #120
Liam MorlandI do not get these Postgres errors when I run the tests on my server. The Postgres tests used to pass on drupal.org, but now give out-of-memory errors. It appears to be a problem with the testing environment, not the patch.
Comment #122
Liam MorlandReroll for 8.8.x. Tests pass on my local in MySQL and Postgres.
Comment #123
MixologicRunning the PostgreSQL on 9.5 appeared to work. Im not sure what else needs to be set for the 9.1 container, or if it can even be fixed.
Comment #124
Liam MorlandReroll.
@Mixologic: Thanks for your help with Postgres testing.
Update: Still a Postgres failure, but not one that appears to be related to the patch.
Update: It seems that Postgres 9.5 testing has been fixed by #3078639: PostgreSQL automated test failing for JsonApiRegressionTest after latest changes on json_api FieldItemNormalizer.
Comment #125
Liam MorlandRe-roll.
Comment #127
Liam MorlandRe-rolling for Drupal 9.
Comment #129
Liam MorlandThe 8.9.x test was accidental. The 9.0.x test passes.
Comment #130
Liam MorlandRe-roll.
Comment #131
Liam MorlandRe-roll.
Comment #132
xjmComment #133
Liam MorlandRe-roll.
Comment #135
David StraussThis looks really good, but I'd love to see these small changes:
Comment #136
Liam MorlandThanks very much for the review. I will update the patch shortly.
RESTRICT and NO ACTION do the same thing in MySQL. In databases such as Postgres, NO ACTION is deferred to the end of the transaction while RESTRICT is not. In most cases, this doesn't make a practical difference, so I don't think anyone would be mislead.
I'm happy to switch to constants.
Comment #137
David StraussAh, I misunderstood the constraint's effect by assuming the name was descriptive. That, plus the disparity in function between PostgreSQL and MySQL makes me feel even more resolve that we should not offer "NO ACTION" as an available foreign key constraint. Deferring enforcement to the end of a transaction seems neat given how we handle transactions (really, labels) in conjunction with hooks, but I also suspect it allows sloppiness in the form of violating presumed FK behavior during larger transactions. Meanwhile, code expecting FK constraints to hold may get invoked before the transaction completes (though it seems like PostgreSQL may raise an error if it does).
Technically, NO ACTION is more standard in the SQL spec than RESTRICT, but I feel that the latter is much more descriptive. FWIW, SQL Server seems to support NO ACTION but not RESTRICT, but it behaves like MySQL/PostgreSQL's RESTRICT in the instant enforcement. So, while the terms differ, the only behavior that seems portable is equivalent to MySQL's RESTRICT.
Finally, does this establish default behavior for a foreign key, or does that fall onto the database? MySQL's default behavior is RESTRICT, but PostgreSQL's is NO ACTION. I feel like we should explicitly default to RESTRICT so there aren't silently different default constraints based on the database in use.
Comment #138
Liam MorlandThe default would be set by the database, but the way the patch is written, it always specifies the action anyway.
I will adjust the patch to not accept NO ACTION.
Comment #139
Liam MorlandI'm not sure about the constants. I could do this:
const SET_DEFAULT = 'SET DEFAULT';
That seems like it is not really helping any.
I could do it with integers:
const SET_DEFAULT = 4;
Then at the point where the SQL is actually generated, I need to have a mapping back to the string
'SET DEFAULT'
. Again, I'm not sure this is making things easier.Using constants means instead of:
A developer would need to have a
use
statement and then:It is not clear to me that this is better DX.
Any thoughts about whether we should use strings, integers, or not bother with constants?
Comment #140
Liam MorlandThis is #133 with 'no action' removed.
Update: I don't know why this is failing in MySQL. It used to pass and hasn't changed except for re-rolls and removing 'no action'. I'll keep working on it.
Comment #142
Liam MorlandThis was failing in MySQL because table name prefixing has changed. In this patch, the prefix is added manually in foreignKeyExists() to be compatible with this change.
Comment #143
Liam MorlandI'm looking for feedback about the above patch and about the use of constants, see #139.
Comment #144
David StraussI prefer constants over strings for a few reasons:
I don't have a strong opinion on which backing values the constants should have. Using string values for them mostly seems fine, but it has a bit more risk of developers simply using the actual strings in their schema definitions rather than the constants. Integers would have a stronger influence on developers following documented practices, as no one would want their schema constraints specified in their code literally as numbers.
Comment #145
David StraussRegarding concrete cases:
On SQL Server,
Database::RESTRICT
would map to the stringNO ACTION
in the actual schema generation. I think that case makes it easier to see why a mapping step is not just a pointless abstraction.I could also see an option for PostgreSQL users to configure their DB to relax
Database::RESTRICT
to being sent asNO ACTION
to allow more flexible enforcement during transactions in progress. Given that most code for Drupal is only tested against MySQL, this would be more useful to PostgreSQL users than having a direct ability to specifyNO ACTION
in schema data structures. Developers testing against MySQL are unlikely to useNO ACTION
versusRESTRICT
, at least in any coherent, useful way to a PostgreSQL user (given that MySQL doesn't distinguish).So, I don't see the constants as simply another way to pick the string being sent to the DB.
Comment #146
Liam MorlandThanks very much for the feedback. Good points about the benefits of constants. This patch uses integer constants.
Comment #147
Liam MorlandRe-roll.
Comment #148
Liam MorlandRe-roll.
Comment #149
Liam MorlandRe-roll.
Comment #150
David StraussThis patch looks good to me (and has for a while). Is there accounting for what stands between where we are and applying it?
Comment #151
Liam MorlandAs far as I know, this is ready for commit. Is there a change record needed for adding a feature like this?
Comment #152
Liam MorlandRe-roll.
Comment #154
Liam MorlandDeprecated test method removed.
I'm not sure what to to about the coding standards problem, "Exceptions should not be translated." Should I just remove the calls to t() and just pass the full error message including variable substitutions as a single string?
Comment #160
nod_D10 version needed
At this time we would need a D10.1.x patch or MR for this issue.
careful reroll
The patch can be tricky to reroll and some code might get lost in the process. When rerolling this issue please make sure all the parts of the patch are kept (new files, new tests, all the changes to the different parts of the file, changes to es6 files ported to .js files, etc.)