Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify multi-column foreign key handling #2212

Merged
merged 1 commit into from
Sep 19, 2023

Conversation

ndm2
Copy link
Contributor

@ndm2 ndm2 commented Aug 29, 2023

Multi-column foreign key support is currently very much broken, and there is no consistency in the behavior of dropForeignKey() and hasForeignKey() accross the various DBMS.

Currently the behavior looks like this:

MySQL Postgres Sqlite SQL Server
Case-sensitive drop no yes yes no¹
Case-sensitive lookup yes yes no yes¹
Multi-column drop no² yes no³ no²
Drop all column matches yes yes no yes
Column order dependent drop no no no no
Column order dependent lookup yes no no no
Silently drop non-existent by columns no yes no yes
Silently drop non-existent by name no no - no

1) In a case sensitive database the query would produce an error.
2) Generates duplicate drop instructions, one for every key that has any of the given columns.
3) For every column, the first key that starts with that column would be deleted.

With these changes, the new behavior would be as follows:

MySQL Postgres Sqlite SQL Server
Case-sensitive drop no yes¹ no yes¹
Case-sensitive lookup no yes¹ no yes¹
Multi-column drop yes yes yes yes
Drop all column matches yes yes yes yes
Column order dependent drop yes yes yes yes
Column order dependent lookup yes yes yes yes
Silently drop non-existent by columns no no no no
Silently drop non-existent by name no no - no

1) The behavior is independent of whether the database/column is actually case-sensitive.

My reasoning for making dropping and looking up foreigny keys by column require to pass the columns in the exact order in which the columns appear in the foreign key definition, is the fact that it is possible to define multiple foreign keys using the same columns. Whether it makes sense to do that, well, I don't really know, but it's very much possible, so I'd rather be strict about it, and have for example dropForeignKey(['a', 'b']) only drop foreign keys that where defined as (a, b), and not the ones defined as (b, a). In the end consistency might be more important though, so please feel free to disagree, changing the behavior is easy enough (Sqlite might be a bit tricky).

Dropping all foreign keys that match the given columns in the correct order might be contentiuous. Not doing so would however mean that one would have to generate one call for each of those keys, and there would not really be any control over which key is being deleted exactly, it would simply be the first match, so it's not like one could drop only a specific key in a group of keys with the same order of columns anyways.

On a related note, the foreign key lookup queries for Postgres and SqlServer produce a lot of duplicates, so those queries might need to be optimized, but I don't really wanted to touch that right now, there's too many pitfalls for my taste, so I've chosen the easy route and just reduced the results, that's good enough to me for now. I only fixed up the ordering for Postgres, as it was sorting by the column order in the related unique constraint, not by the column order in the actual foreign key constraint.

Also this PR relaxes the foreign key SQL parsing for Sqlite, for better compatibility with existing databases that may not adhere to the exact syntax that Phinx creates.

@ndm2 ndm2 force-pushed the 1.x-fix-unify-multi-col-fk-handling branch from 563266e to f2c75ea Compare September 3, 2023 15:03
@MasterOdin
Copy link
Member

As a bookkeeping question, does this need to land in the 1.x branch, or could we land it in 0.x and have this be put out with a new version there? I'm not sure we've totally established what 1.x is.

@ndm2
Copy link
Contributor Author

ndm2 commented Sep 5, 2023

Well, I never really understood why Phinx has this 0.x release schema, and I'm not sure when possible breaking changes like this one can be introduced there, especially since 0.next seems unused doesn't exist anymore.

To my understanding 1.x (or whatever it might be released as) is primarily meant to be the CakePHP 5 compatible version, and I stumbled over this multi-column problem while working on the CakePHP 5 compatible version of the Migrations plugin, hence I targeted 1.x here first.

If it can go in 0.x too, I could either backport it, or target 0.x and pull it into 1.x via a 0.x -> 1.x merge.

@dereuromark dereuromark added the bug label Sep 7, 2023
@dereuromark
Copy link
Member

We kept it 0.x to clarify the Api is still somewhat not stable and any major could have some Bagger BC breaking changes than one would expect on stable API.

Copy link
Member

@MasterOdin MasterOdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some clarifying questions, otherwise I think this code is good to merge, thanks for including comprehensive tests, and that nothing got broken that we already testing for which is nice.

The only potential complaint I could see is that it does potentially make removing all FKs for a given column more annoying, as instead of writing ->dropForeignKey('foo'), you'd maybe have to write several with exact usage. However, I'd argue that's probably better that we force people to be more exact when futzing with their DB, and prevent them from putting themselves into an unexpected state, where they were actually looking to drop the FK on (foo) but not (foo, bar), but not realizing it.

We'll want to figure out what the versioning story for phinx is before we merge this, since I guess we're now on v2, and this'll put us on v3?

@@ -952,7 +952,7 @@ protected function getForeignKeys(string $tableName): array
JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name
WHERE constraint_type = 'FOREIGN KEY' AND tc.table_schema = %s AND tc.table_name = %s
ORDER BY kcu.position_in_unique_constraint",
ORDER BY kcu.ordinal_position",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding, we don't want to use position_in_unique_constraint as this would give us the order of columns within the index clause on the reference table, while ordinal_position gives us the order within the FK on the table, and that these two orders are not guaranteed to match.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, the order of columns on the implicitly created index on the target table will always be in the order in which the columns appear in the source table definition, irrespective of the order of columns in the foreign key constraint definition.

@@ -962,6 +962,10 @@ protected function getForeignKeys(string $tableName): array
$foreignKeys[$row['constraint_name']]['referenced_table'] = $row['referenced_table_name'];
$foreignKeys[$row['constraint_name']]['referenced_columns'][] = $row['referenced_column_name'];
}
foreach ($foreignKeys as $name => $key) {
$foreignKeys[$name]['columns'] = array_values(array_unique($key['columns']));
$foreignKeys[$name]['referenced_columns'] = array_values(array_unique($key['referenced_columns']));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've added this since you were seeing duplicates from the above query?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, due to the nature of the joins, every additional column will cause a duplicate, so a foreign key over foo, bar, baz would result in a columns list holding foo, foo, foo, bar, bar, bar, baz, baz, baz.

@@ -224,7 +224,7 @@ public function hasTable(string $tableName): bool
}

/** @var array<string, mixed> $result */
$result = $this->fetchRow(sprintf("SELECT count(*) as [count] FROM information_schema.tables WHERE table_name = '%s';", $tableName));
$result = $this->fetchRow(sprintf("SELECT count(*) as [count] FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '%s';", $tableName));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalizing all these references on the case using a case-sensitive SQL Server installation, which then requires using uppercase?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, otherwise it will trigger an error. I also tried to make the tests run on a case sensitive database, but somehow in the workflow environment using a case sensitive collation always causes the server to crash, while it's working fine locally. So far I wasn't able to figure out what the problem is.

@MasterOdin MasterOdin changed the title 1.x - Fix up and unify multi-column foreign key handling. Unify multi-column foreign key handling Sep 11, 2023
@ndm2
Copy link
Contributor Author

ndm2 commented Sep 13, 2023

We'll want to figure out what the versioning story for phinx is before we merge this, since I guess we're now on v2, and this'll put us on v3?

I don't really know, I was kinda blindsided by the CakePHP 5 release, I must've missed the discussion to change the release date, I was under the impression it would be shortly before/after Cakefest. So I don't really know what do with this now 🤷

@MasterOdin MasterOdin changed the base branch from 1.x to 0.x September 19, 2023 08:04
@MasterOdin MasterOdin merged commit c2068c1 into 0.x Sep 19, 2023
11 checks passed
@MasterOdin MasterOdin deleted the 1.x-fix-unify-multi-col-fk-handling branch September 19, 2023 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants