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.
CommentFileSizeAuthor
#154 drupal-foreign_key-2254131-154-D9.patch24.44 KBLiam Morland
#154 interdiff-152-154.txt2.89 KBLiam Morland
#152 drupal-foreign_key-2254131-152-D9.patch24.47 KBLiam Morland
#149 drupal-foreign_key-2254131-149-D9.patch24.47 KBLiam Morland
#148 drupal-foreign_key-2254131-148-D9.patch24.47 KBLiam Morland
#147 drupal-foreign_key-2254131-147-D9.patch24.47 KBLiam Morland
#146 interdiff-142-146.txt8.97 KBLiam Morland
#146 drupal-foreign_key-2254131-146-D9.patch24.47 KBLiam Morland
#142 interdiff-140-142.txt1.08 KBLiam Morland
#142 drupal-foreign_key-2254131-142-D9.patch22.74 KBLiam Morland
#140 interdiff-133-140.txt3.28 KBLiam Morland
#140 drupal-foreign_key-2254131-140-D9.patch22.69 KBLiam Morland
#133 drupal-foreign_key-2254131-133-D9.patch22.74 KBLiam Morland
#131 drupal-foreign_key-2254131-131-D9.patch22.74 KBLiam Morland
#130 drupal-foreign_key-2254131-130-D9.patch22.73 KBLiam Morland
#127 drupal-foreign_key-2254131-127-D9.patch22.73 KBLiam Morland
#125 drupal-foreign_key-2254131-125-D8.patch22.73 KBLiam Morland
#124 drupal-foreign_key-2254131-124-D8.patch22.95 KBLiam Morland
#122 drupal-foreign_key-2254131-122-D8.patch22.95 KBLiam Morland
#118 drupal-foreign_key-2254131-118-D8.patch22.88 KBLiam Morland
#116 drupal-foreign_key-2254131-112-D8.patch22.87 KBLiam Morland
#111 2254131-110.patch22.9 KBjofitz
#111 interdiff-2254131-108-110.txt2.19 KBjofitz
#108 drupal-foreign_key-2254131-108-D8.patch22.76 KBLiam Morland
#106 interdiff-2254131-105-106.txt2.09 KBLiam Morland
#106 drupal-foreign_key-2254131-106-D8.patch22.62 KBLiam Morland
#105 interdiff-2254131-103-105.txt5.37 KBLiam Morland
#105 drupal-foreign_key-2254131-105-D8.patch22.83 KBLiam Morland
#103 interdiff-2254131-102-103.txt777 bytesLiam Morland
#103 drupal-foreign_key-2254131-103-D8.patch24.25 KBLiam Morland
#102 interdiff-2254131-101-102.txt10.64 KBLiam Morland
#102 drupal-foreign_key-2254131-102-D8.patch24.24 KBLiam Morland
#101 interdiff-93-101.txt5.98 KBLiam Morland
#101 drupal-foreign_key-2254131-101-D8.patch20.38 KBLiam Morland
#3 core_2254131_allow_fk.patch5.67 KBLiam Morland
#31 drupal-foreign_key-2254131-31-D8.patch15.29 KBLiam Morland
#33 drupal-foreign_key-2254131-33-D8.patch15.32 KBLiam Morland
#35 drupal-foreign_key-2254131-36-D8.patch15.41 KBLiam Morland
#38 drupal-foreign_key-2254131-37-D8.patch15.43 KBgaurav.kapoor
#38 interdiff-36-37.txt2.11 KBgaurav.kapoor
#40 2254131-40.patch15.36 KBjofitz
#47 2254131-47.patch15.39 KBjofitz
#51 drupal-foreign_key-2254131-51-D8.patch15.39 KBLiam Morland
#65 drupal-foreign_key-2254131-65-D8.patch16.62 KBLiam Morland
#59 drupal-foreign_key-2254131-59-D8.patch14.61 KBLiam Morland
#66 interdiff-2254131-59-65.txt16.63 KBLiam Morland
#69 drupal-foreign_key-2254131-69-D8.patch16.63 KBLiam Morland
#88 drupal-foreign_key-2254131-88-D8.patch4.49 KBLiam Morland
#88 interdiff-2254131-69-88.patch4.49 KBLiam Morland
#91 drupal-foreign_key-2254131-91-D8.patch20.1 KBLiam Morland
#93 drupal-foreign_key-2254131-93-D8.patch20.1 KBLiam Morland
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Liam Morland’s picture

davidwbarratt’s picture

I support this!

Liam Morland’s picture

Status: Active » Needs review
FileSize
5.67 KB

Patch for MySQL and Postgres.

Crell’s picture

I'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...

Liam Morland’s picture

This 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.

davidwbarratt’s picture

Liam,

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!

Liam Morland’s picture

That 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.

davidwbarratt’s picture

Liam,

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!

Liam Morland’s picture

I 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?

ar-jan’s picture

It looks like the question is whether Crell's concern in #4 is justified or not:

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...

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.

Liam Morland’s picture

Say you implemented your own schema with foreign keys, would/could that break any of Drupal's funcionality?

No. 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.

ar-jan’s picture

Right, 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.

Liam Morland’s picture

I 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.

Crell’s picture

The 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.

davidwbarratt’s picture

Crell,

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!

Crell’s picture

That 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.

davidwbarratt’s picture

Crell,

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!

Liam Morland’s picture

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.

The 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.

There are extremely few cases where a non-site-specific module should ever be writing SQL anymore, which makes most of this issue moot.

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.

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.

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.

Another way of solving this problem is by allowing NULL on fields with database level foreign keys.

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.

Crell’s picture

Allowing 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.)

Liam Morland’s picture

How 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?

Liam Morland’s picture

For example, one might configure a FK like this:

    'foreign keys' => array(
      'node_revision' => array(
        'table' => 'node_revision',
        'columns' => array('vid' => 'vid'),
        'on update' => 'cascade',
        'on delete' => 'set null',
      ),
    ),
Crell’s picture

Issue tags: +rc deadline

I 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.

Liam Morland’s picture

Thanks very much, Crell. I agree with the requirements. I will work on this.

Liam Morland’s picture

xjm’s picture

Version: 8.0.x-dev » 8.1.x-dev
Status: Needs review » Postponed
Issue tags: -rc deadline

This 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.

Liam Morland’s picture

Issue summary: View changes

Update issue summary to match current state of this initiative.

Liam Morland’s picture

Issue summary: View changes

Added list of valid values for on update/delete.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Liam Morland’s picture

Status: Postponed » Needs review
FileSize
15.29 KB

The attached patch implements foreign keys and tests.

Status: Needs review » Needs work

The last submitted patch, 31: drupal-foreign_key-2254131-31-D8.patch, failed testing.

Liam Morland’s picture

Status: Needs work » Needs review
FileSize
15.32 KB

Status: Needs review » Needs work

The last submitted patch, 33: drupal-foreign_key-2254131-33-D8.patch, failed testing.

Pavan B S’s picture

Assigned: Unassigned » Pavan B S
Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -366,6 +376,18 @@ protected function createKeySql($fields) {
    +   *   The SQL definition of the foreign key or NULL if the definition is invalid.
    

    Line exceeding 80 characters

  2. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
    @@ -443,6 +454,18 @@ protected function _createKeySql($fields) {
    +   *   The SQL definition of the foreign key or NULL if the definition is invalid.
    

    Line exceeding 80 characters

  3. +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
    @@ -691,11 +691,23 @@ protected function tearDown() {
    +      // MySQL does not have a transactional DDL so we must disable foreign keys to
    

    Line exceeding 80 characters

Pavan B S’s picture

Issue tags: +Needs reroll
  1. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -366,6 +376,18 @@ protected function createKeySql($fields) {
    +   *   The SQL definition of the foreign key or NULL if the definition is invalid.
    

    Line exceeding 80 characters

  2. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
    @@ -443,6 +454,18 @@ protected function _createKeySql($fields) {
    +   *   The SQL definition of the foreign key or NULL if the definition is invalid.
    

    Line exceeding 80 characters

  3. +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
    @@ -691,11 +691,23 @@ protected function tearDown() {
    +      // MySQL does not have a transactional DDL so we must disable foreign keys to
    

    Line exceeding 80 characters

gaurav.kapoor’s picture

Status: Needs work » Needs review
FileSize
15.43 KB
2.11 KB

Status: Needs review » Needs work

The last submitted patch, 38: drupal-foreign_key-2254131-37-D8.patch, failed testing.

jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
15.36 KB

Re-rolled.

EDIT: Unable to create interdiff due to re-roll (as is often the case):

interdiff: Error applying patch1 to reconstructed file

colan’s picture

#40: Please provide an interdiff to illustrate any differences when providing updated patches.

gaurav.kapoor’s picture

Status: Needs review » Reviewed & tested by the community

All the minor fixes have been resolved.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 40: 2254131-40.patch, failed testing.

daffie’s picture

Issue tags: +Needs reroll

For the array's use the short notation [] and not the long version array().

gaurav.kapoor’s picture

Assigned: Pavan B S » gaurav.kapoor
gaurav.kapoor’s picture

Assigned: gaurav.kapoor » Unassigned
jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
15.39 KB

Re-rolled.

Unable to create interdiff due to complexity of re-roll:

interdiff impossible; taking evasive action

lostcarpark’s picture

I'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!

Liam Morland’s picture

Thanks. Could you review the patch?

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Liam Morland’s picture

Peter Majmesku’s picture

Is this just for database table schemas or also for entities?

How do we handle migration/update of existing databases without foreign keys?

Liam Morland’s picture

This 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.

Peter Majmesku’s picture

Does this work for content/config entities or just for table schemas (= hook_schema() definitions)?

Liam Morland’s picture

This patch only affects database tables.

Peter Majmesku’s picture

Why not extend it, to work with entities also?

Liam Morland’s picture

I don't know what that would mean. Make a child ticket. This ticket has a narrow focus on just this one feature.

geek-merlin’s picture

Patch #51: Nice work! Code looks straightforward.

One nit though:

private function fkExists($table, $name) {

I'd write that out like everything else.

Liam Morland’s picture

Status: Needs review » Needs work

The last submitted patch, 59: drupal-foreign_key-2254131-59-D8.patch, failed testing. View results

Liam Morland’s picture

Status: Needs work » Needs review
Peter Majmesku’s picture

Status: Needs review » Needs work

The 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.

Liam Morland’s picture

Yes, 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.

Peter Majmesku’s picture

Can you prove the same failing tests, when you are testing without any code changes? That would prove your assumption.

Liam Morland’s picture

This 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.

Liam Morland’s picture

FileSize
16.63 KB
Liam Morland’s picture

Status: Needs review » Needs work

That failure I think I can fix. Stand by.

Peter Majmesku’s picture

@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".

Liam Morland’s picture

Issue summary: View changes
Liam Morland’s picture

Status: Needs review » Needs work

Clearly, 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.

Liam Morland’s picture

Issue tags: +PostgreSQL
Peter Majmesku’s picture

Issue tags: -PostgreSQL

Can you post me the instruction for testing your patch?

Liam Morland’s picture

Issue tags: +PostgreSQL

The 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.

Peter Majmesku’s picture

Can I also create a custom entity with foreign keys on it's relation?

Liam Morland’s picture

No, this ticket is only about the database abstraction layer's ability to manage database FKs.

Peter Majmesku’s picture

SchemaTest::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

$is_mysql = Database::getConnection()->databaseType() === 'mysql';

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?

Peter Majmesku’s picture

Status: Needs work » Reviewed & tested by the community

I 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.

Peter Majmesku’s picture

I have created an issue for refactoring SchemaTest::testSchema() at https://www.drupal.org/project/drupal/issues/2935506.

Liam Morland’s picture

The patch has passed with PHP 5.5 and PHP 7 on all three supported databases.

daffie’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: -PostgreSQL

@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:

  1. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -382,6 +382,20 @@ public function fieldExists($table, $column) {
    +   * @return bool|null
    +   *   TRUE if the foreign key exists, FALSE if it does not, NULL if the
    +   *   database does not support foreign keys.
    

    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.

  2. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,68 @@ public function fieldExists($table, $column) {
    +   *     'on update' and 'on delete' => One of 'no action', 'restrict',
    +   *       'cascade', 'set null', or 'set default'.
    

    Can we have some more documentation what these options are and what they do. Are they required values? For both on update and on delete? It is not clear to me how this works.

  3. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,68 @@ public function fieldExists($table, $column) {
    +   * @return true|null
    +   *   TRUE if the foreign key was created, NULL if the database does not
    +   *   support foreign keys.
    ...
    +    if (!method_exists($this, 'createForeignKeySql')) {
    +      return NULL;
    +    }
    

    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.

  4. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,68 @@ public function fieldExists($table, $column) {
    +   * @return
    +   *   TRUE if the key was successfully dropped, FALSE if there was no key by
    +   *   that name to begin with, NULL if the database does not support foreign
    +   *   keys.
    

    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.

  5. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,68 @@ public function fieldExists($table, $column) {
    +  public function dropForeignKey($table, $name) {
    +    $foreignKeyExists = $this->foreignKeyExists($table, $name);
    +
    +    if ($foreignKeyExists) {
    +      $this->connection->query('ALTER TABLE {' . $table . '} ' . $this->dropForeignKeySql($table, $name));
    +      return TRUE;
    +    }
    +    return $foreignKeyExists;
    +  }
    

    This can be rewritten to:

    +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,68 @@ public function fieldExists($table, $column) {
    +  public function dropForeignKey($table, $name) {
    +    if ($this->foreignKeyExists($table, $name)) {
    +      $this->connection->query('ALTER TABLE {' . $table . '} ' . $this->dropForeignKeySql($table, $name));
    +      return TRUE;
    +    }
    +    return FALSE;
    +  }
    

    We could add an extra test to see if the foreign key was really dropped.

  6. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
    @@ -535,7 +582,7 @@ public function dropTable($table) {
    -    $this->connection->query('DROP TABLE {' . $table . '}');
    +    $this->connection->query('DROP TABLE {' . $table . '} CASCADE');
    

    Can there be some documentation added why the "CASCADE" keyword needs to be added.

  7. +++ b/core/modules/simpletest/simpletest.module
    @@ -671,6 +671,14 @@ function simpletest_clean_environment() {
    +  // Use transactional DDL if available.
    +  $txn = Database::getConnection()->startTransaction();
    +  // MySQL does not have a transactional DDL so we must disable foreign keys to
    +  // delete tables that have fk relationships between them.
    +  $is_mysql = Database::getConnection()->databaseType() === 'mysql';
    +  if ($is_mysql) {
    +    Database::getConnection()->query('SET foreign_key_checks = 0');
    +  }
    
    @@ -681,6 +689,11 @@ function simpletest_clean_database() {
    +  if ($is_mysql) {
    +    Database::getConnection()->query('SET foreign_key_checks = 1');
    +  }
    +  // Commit the transaction.
    +  unset($txn);
    
    +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
    @@ -634,11 +634,25 @@ protected function tearDown() {
    +      // Use transactional DDL if available.
    +      $txn = Database::getConnection()->startTransaction();
    +      // MySQL does not have a transactional DDL so we must
    +      // disable foreign keys to delete tables that have
    +      // fk relationships between them.
    +      $is_mysql = Database::getConnection()->databaseType() === 'mysql';
    +      if ($is_mysql) {
    +        Database::getConnection()->query('SET foreign_key_checks = 0');
    +      }
    ...
    +      if ($is_mysql) {
    +        Database::getConnection()->query('SET foreign_key_checks = 1');
    +      }
    +      // Commit the transaction.
    +      unset($txn);
    

    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.

  8. The changes to “core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php” are all to the test testSchema(). This is already a huge test. Let’s not make it any bigger. See #2935506: Refactor SchemaTest::testSchema() to make it more granular. Create new tests for the new functionality. Not only for when things go as they should go, but also when they should fail.
Liam Morland’s picture

Thanks @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.

daffie’s picture

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.

Most 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.

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.

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.

Liam Morland’s picture

The 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.

daffie’s picture

The tests run with foreign_key_checks set to 1. It is only set to 0 for tear-down and clean-up.

You are right.

Liam Morland’s picture

What 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.

daffie’s picture

What 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.

Somehow 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.

Liam Morland’s picture

The last submitted patch, 88: drupal-foreign_key-2254131-88-D8.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 88: interdiff-2254131-69-88.patch, failed testing. View results

Liam Morland’s picture

Sorry about that. The interdiff above is the correct file. The differences are all in the comments, so testing results should be the same.

Liam Morland’s picture

Liam Morland’s picture

Peter Majmesku’s picture

Status: Needs review » Reviewed & tested by the community

Looks good. How about commiting it into core?

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs work

The 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.

Peter Majmesku’s picture

Are the failing tests related to this patch? I saw them passing.

Liam Morland’s picture

Status: Needs work » Needs review

Everything passes now. The failures did not appear to be related to the changes.

Peter Majmesku’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me! Thank you for your work Liam Morland. Much appreciated.

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -278,6 +278,16 @@ protected function createKeysSql($spec) {
    +          $fk = $this->createForeignKeySql(NULL, $key, $fields);
    +          if ($fk) {
    +            $keys[] = $fk;
    

    nit: I would prefer we named this $foreign_key for clarity

  2. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -370,6 +380,41 @@ protected function createKeySql($fields) {
    +  protected function createForeignKeySql($table, $key_name, array $fields) {
    ...
    +  protected function dropForeignKeySql($table, $name) {
    

    If the signature here was

    protected function createForeignKeySql($key_name, array $fields, $table = NULL);
    

    I think it would make more sense, having a largely optional parameter first feels skewif

    Same story on the drop

  3. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -370,6 +380,41 @@ protected function createKeySql($fields) {
    +    if (isset($fields['on update']) && isset($fields['on delete']) && in_array($fields['on update'], $operations)  && in_array($fields['on delete'], $operations)) {
    

    extra space here.

    in_array should use the third argument in both instances, we're checking strings here.

    https://3v4l.org/DWjeK

  4. +++ b/core/lib/Drupal/Core/Database/Driver/mysql/Schema.php
    @@ -370,6 +380,41 @@ protected function createKeySql($fields) {
    +      return 'CONSTRAINT `' . $key_name . '` FOREIGN KEY (`' . implode('`, `', array_keys($fields['columns'])) . '`) REFERENCES {' . $fields['table'] . '} (`' . implode('`, `', $fields['columns']) . '`) ON DELETE ' . strtoupper($fields['on delete']) . ' ON UPDATE ' . strtoupper($fields['on update']);
    

    in my opinion this would be far more readable if it used sprintf

  5. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
    @@ -442,6 +454,41 @@ protected function _createKeySql($fields) {
    +    $operations = ['no action', 'restrict', 'cascade', 'set null', 'set default'];
    +    if (isset($fields['on update']) && isset($fields['on delete']) && in_array($fields['on update'], $operations)  && in_array($fields['on delete'], $operations)) {
    

    We have this code in multiple places - would it make sense as a protected method on the base class?

  6. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Schema.php
    @@ -535,7 +582,10 @@ public function dropTable($table) {
    +    $this->connection->query('DROP TABLE {' . $table . '} CASCADE');
    

    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?

  7. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -382,6 +382,20 @@ public function fieldExists($table, $column) {
    +  public function foreignKeyExists($table, $name) {}
    

    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)

  8. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,70 @@ public function fieldExists($table, $column) {
    +      throw new SchemaObjectDoesNotExistException(t("Cannot add foreign key @name to table @table: table doesn't exist.", ['@table' => $table, '@name' => $name]));
    ...
    +      throw new SchemaObjectExistsException(t("Cannot add foreign key @name to table @table: foreign key already exists.", ['@table' => $table, '@name' => $name]));
    ...
    +      throw new SchemaException(t("Cannot add foreign key @name to table @table: invalid foreign key definition.", ['@table' => $table, '@name' => $name]));
    

    We need tests for these code paths

  9. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,70 @@ public function fieldExists($table, $column) {
    +    $createForeignKeySql = $this->createForeignKeySql($table, $name, $fields);
    +    if (!$createForeignKeySql) {
    

    we can probably inline this

    if (!$createForeignKeySql = $this->createForeignKeySql($table, $name, $fields)) {
    
  10. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,70 @@ public function fieldExists($table, $column) {
    +    $this->connection->query('ALTER TABLE {' . $table . '} ADD ' . $createForeignKeySql);
    

    Should we check the return of this before we return TRUE?

  11. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -409,6 +423,70 @@ public function fieldExists($table, $column) {
    +      $this->connection->query('ALTER TABLE {' . $table . '} ' . $this->dropForeignKeySql($table, $name));
    +      return TRUE;
    

    Same, should we check the return of ->query before returning TRUE?

  12. +++ b/core/modules/simpletest/simpletest.module
    @@ -671,6 +671,14 @@ function simpletest_clean_environment() {
    +  // delete tables that have fk relationships between them.
    
    +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
    @@ -634,11 +634,25 @@ protected function tearDown() {
    +      $is_mysql = Database::getConnection()->databaseType() === 'mysql';
    +      if ($is_mysql) {
    ...
    +      if ($is_mysql) {
    

    can we use foreign key here instead of fk, for clarity sake

  13. +++ b/core/modules/simpletest/simpletest.module
    @@ -671,6 +671,14 @@ function simpletest_clean_environment() {
    +  $is_mysql = Database::getConnection()->databaseType() === 'mysql';
    +  if ($is_mysql) {
    
    @@ -681,6 +689,11 @@ function simpletest_clean_database() {
    +  if ($is_mysql) {
    
    +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
    @@ -634,11 +634,25 @@ protected function tearDown() {
    +      $is_mysql = Database::getConnection()->databaseType() === 'mysql';
    +      if ($is_mysql) {
    ...
    +      if ($is_mysql) {
    

    special casing mysql feels wrong, what if other contrib drivers are in the same boat?

    sounds like we're missing a supportsTransactionalDataDefinitionStatements method or similar

  14. +++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
    @@ -179,6 +192,20 @@ public function testSchema() {
    +          'table' => 'fk_test_table',
    +          'columns' => ['test_field' => 'id'],
    +          'on update' => 'cascade',
    +          'on delete' => 'set null',
    +        ],
    +        // The following should not create anything in the database because
    +        // 'on update' and 'on delete' are not set.
    +        'test_fk_no_create' => [
    +          'table' => 'no_table',
    

    There are no tests of invalid values for 'on update' and 'on delete'

Liam Morland’s picture

Thanks 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.

Liam Morland’s picture

This 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.

Liam Morland’s picture

I 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.

Liam Morland’s picture

I 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.

Liam Morland’s picture

This 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.

Liam Morland’s picture

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Liam Morland’s picture

Status: Needs review » Needs work

The last submitted patch, 108: drupal-foreign_key-2254131-108-D8.patch, failed testing. View results

Liam Morland’s picture

Status: Needs work » Needs review
FileSize
22.77 KB

Remove deprecated db_create_table().

Peter Majmesku’s picture

Status: Needs review » Reviewed & tested by the community

LGTM

The last submitted patch, 106: drupal-foreign_key-2254131-106-D8.patch, failed testing. View results

Liam Morland’s picture

Postgres 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.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 111: 2254131-110.patch, failed testing. View results

Liam Morland’s picture

Status: Needs work » Needs review
FileSize
22.87 KB

Reroll.

Status: Needs review » Needs work

The last submitted patch, 116: drupal-foreign_key-2254131-112-D8.patch, failed testing. View results

Liam Morland’s picture

Liam Morland’s picture

Issue summary: View changes

Switch to short array syntax.

Liam Morland’s picture

I 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.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Liam Morland’s picture

FileSize
22.95 KB

Reroll for 8.8.x. Tests pass on my local in MySQL and Postgres.

Mixologic’s picture

Running 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.

Liam Morland’s picture

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Status: Needs review » Needs work

The last submitted patch, 127: drupal-foreign_key-2254131-127-D9.patch, failed testing. View results

Liam Morland’s picture

Status: Needs work » Needs review

The 8.9.x test was accidental. The 9.0.x test passes.

xjm’s picture

Version: 9.0.x-dev » 9.1.x-dev

Status: Needs review » Needs work

The last submitted patch, 133: drupal-foreign_key-2254131-133-D9.patch, failed testing. View results

David Strauss’s picture

This looks really good, but I'd love to see these small changes:

  • Remove the ability to add a foreign key with update or delete set to "no action." While databases may support that, I don't really see the use case, as it basically misleads someone reviewing the schema into assuming the data has properties that aren't actually enforced. I have a strong opinion that we should expose only options with demonstrated need rather than attempting to expose the entire feature set of underlying DBs via the abstraction layer.
  • Switch from strings for specifying the effects/enforcement of the foreign key to constants. This is not such a strong opinion, but I think it would be better DX.
Liam Morland’s picture

Thanks 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.

David Strauss’s picture

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.

Ah, 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.

Liam Morland’s picture

Issue summary: View changes

The 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.

Liam Morland’s picture

I'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:

    'foreign keys' => [
        ...
        'on update' => 'cascade',
        'on delete' => 'set null',
      ],
    ],

A developer would need to have a use statement and then:

    'foreign keys' => [
        ...
        'on update' => Database::CASCADE,
        'on delete' => Database::SET_NULL,
      ],
    ],

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?

Liam Morland’s picture

This 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.

Status: Needs review » Needs work

The last submitted patch, 140: drupal-foreign_key-2254131-140-D9.patch, failed testing. View results

Liam Morland’s picture

This 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.

Liam Morland’s picture

I'm looking for feedback about the above patch and about the use of constants, see #139.

David Strauss’s picture

I'm looking for feedback about the above patch and about the use of constants, see #139.

I prefer constants over strings for a few reasons:

  • It's easier to know early that the options specified are valid, available ones. With constants, PHP basically evaluates this as the schema data structure gets generated. With strings, it's only evaluated whenever the string gets compared against expected options later. (This is a general problem with using special strings for keys or values, though, not just here.)
  • Strings for fixed values can be a leaky abstraction (or appear to be). It's unclear to a developer whether they can use any string that their database backend would accept for constraints, even if the option isn't available other other DBs. Because our goal is to have schema be portable, this expectation would be incorrect.
  • Even if there's a validated allow list for string values, a string implies direct use of the value in interaction with the DB. Yet, the meanings of the strings in each DB aren't consistent. We may convert a requested constraint to work with a particular DB. For example, we would need to map "RESTRICT" to "NO ACTION" on SQL Server, as it lacks the former but has the latter behave the same as "RESTRICT" on MySQL/PostgreSQL. A constant communicates that the schema reflects the intent of constraints, not a literal string the DB will receive. I think this is the strongest reason to prefer constants. I think schema data structure values specified as strings ought to be be passed literally to the DB (or with the only processing being something like prefixing/escaping).

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.

David Strauss’s picture

Regarding concrete cases:

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.

On SQL Server, Database::RESTRICT would map to the string NO 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 as NO 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 specify NO ACTION in schema data structures. Developers testing against MySQL are unlikely to use NO ACTION versus RESTRICT, 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.

Liam Morland’s picture

Thanks very much for the feedback. Good points about the benefits of constants. This patch uses integer constants.

Liam Morland’s picture

Liam Morland’s picture

Liam Morland’s picture

David Strauss’s picture

This patch looks good to me (and has for a while). Is there accounting for what stands between where we are and applying it?

Liam Morland’s picture

As far as I know, this is ready for commit. Is there a change record needed for adding a feature like this?

Liam Morland’s picture

Status: Needs review » Needs work

The last submitted patch, 152: drupal-foreign_key-2254131-152-D9.patch, failed testing. View results

Liam Morland’s picture

Deprecated 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?

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

nod_’s picture

Status: Needs review » Needs work
Issue tags: +Needs careful reroll

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.)

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.