From cce96afe9b417a7908fa3a4ea89633da56f62998 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Fri, 20 Dec 2024 16:11:01 -0500 Subject: [PATCH] Port changes from cakephp/phinx#2322 Port changes to add deferrable foreign key support to postgres. --- docs/en/writing-migrations.rst | 1 + src/Db/Adapter/PostgresAdapter.php | 3 ++ src/Db/Table/ForeignKey.php | 58 +++++++++++++++++++++- tests/TestCase/Db/Table/ForeignKeyTest.php | 37 ++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst index d8eecd54..16b85271 100644 --- a/docs/en/writing-migrations.rst +++ b/docs/en/writing-migrations.rst @@ -881,6 +881,7 @@ Option Description update set an action to be triggered when the row is updated delete set an action to be triggered when the row is deleted constraint set a name to be used by foreign key constraint +deferrable define deferred constraint application (postgres only) ========== =========== You can pass one or more of these options to any column with the optional diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index ee67d88b..65408635 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -1329,6 +1329,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta if ($foreignKey->getOnUpdate()) { $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; } + if ($foreignKey->getDeferrableMode()) { + $def .= " {$foreignKey->getDeferrableMode()}"; + } return $def; } diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php index b12fbfe2..6ab255c5 100644 --- a/src/Db/Table/ForeignKey.php +++ b/src/Db/Table/ForeignKey.php @@ -17,11 +17,14 @@ class ForeignKey public const RESTRICT = 'RESTRICT'; public const SET_NULL = 'SET NULL'; public const NO_ACTION = 'NO ACTION'; + public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED'; + public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE'; + public const NOT_DEFERRED = 'NOT DEFERRABLE'; /** * @var array */ - protected static array $validOptions = ['delete', 'update', 'constraint']; + protected static array $validOptions = ['delete', 'update', 'constraint', 'deferrable']; /** * @var string[] @@ -53,6 +56,11 @@ class ForeignKey */ protected ?string $constraint = null; + /** + * @var string|null + */ + protected ?string $deferrableMode = null; + /** * Sets the foreign key columns. * @@ -191,6 +199,27 @@ public function getConstraint(): ?string return $this->constraint; } + /** + * Sets deferrable mode for the foreign key. + * + * @param string $deferrableMode Constraint + * @return $this + */ + public function setDeferrableMode(string $deferrableMode) + { + $this->deferrableMode = $this->normalizeDeferrable($deferrableMode); + + return $this; + } + + /** + * Gets deferrable mode for the foreign key. + */ + public function getDeferrableMode(): ?string + { + return $this->deferrableMode; + } + /** * Utility method that maps an array of index options to this objects methods. * @@ -210,6 +239,8 @@ public function setOptions(array $options) $this->setOnDelete($value); } elseif ($option === 'update') { $this->setOnUpdate($value); + } elseif ($option === 'deferrable') { + $this->setDeferrableMode($value); } else { $method = 'set' . ucfirst($option); $this->$method($value); @@ -235,4 +266,29 @@ protected function normalizeAction(string $action): string return constant($constantName); } + + /** + * From passed value checks if it's correct and fixes if needed + * + * @param string $deferrable Deferrable + * @throws \InvalidArgumentException + * @return string + */ + protected function normalizeDeferrable(string $deferrable): string + { + $mapping = [ + 'DEFERRED' => ForeignKey::DEFERRED, + 'IMMEDIATE' => ForeignKey::IMMEDIATE, + 'NOT DEFERRED' => ForeignKey::NOT_DEFERRED, + ForeignKey::DEFERRED => ForeignKey::DEFERRED, + ForeignKey::IMMEDIATE => ForeignKey::IMMEDIATE, + ForeignKey::NOT_DEFERRED => ForeignKey::NOT_DEFERRED, + ]; + $normalized = strtoupper(str_replace('_', ' ', $deferrable)); + if (array_key_exists($normalized, $mapping)) { + return $mapping[$normalized]; + } + + throw new InvalidArgumentException('Unknown deferrable passed: ' . $deferrable); + } } diff --git a/tests/TestCase/Db/Table/ForeignKeyTest.php b/tests/TestCase/Db/Table/ForeignKeyTest.php index 573a0b0b..790b1051 100644 --- a/tests/TestCase/Db/Table/ForeignKeyTest.php +++ b/tests/TestCase/Db/Table/ForeignKeyTest.php @@ -97,4 +97,41 @@ public function testSetOptionThrowsExceptionIfOptionIsNotString() $this->fk->setOptions(['update']); } + + #[DataProvider('deferrableProvider')] + public function testDeferrableCanBeSetThroughSetters(string $dirtyValue, string $valueOfConstant): void + { + $this->fk->setDeferrableMode($dirtyValue); + $this->assertEquals($valueOfConstant, $this->fk->getDeferrableMode()); + } + + #[DataProvider('deferrableProvider')] + public function testDeferrableCanBeSetThroughOptions(string $dirtyValue, string $valueOfConstant): void + { + $this->fk->setOptions([ + 'deferrable' => $dirtyValue, + ]); + $this->assertEquals($valueOfConstant, $this->fk->getDeferrableMode()); + } + + public static function deferrableProvider(): array + { + return [ + ['DEFERRED', ForeignKey::DEFERRED], + ['IMMEDIATE', ForeignKey::IMMEDIATE], + ['NOT_DEFERRED', ForeignKey::NOT_DEFERRED], + ['Deferred', ForeignKey::DEFERRED], + ['Immediate', ForeignKey::IMMEDIATE], + ['Not_deferred', ForeignKey::NOT_DEFERRED], + [ForeignKey::DEFERRED, ForeignKey::DEFERRED], + [ForeignKey::IMMEDIATE, ForeignKey::IMMEDIATE], + [ForeignKey::NOT_DEFERRED, ForeignKey::NOT_DEFERRED], + ]; + } + + public function testThrowsErrorForInvalidDeferrableValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->fk->setDeferrableMode('invalid_value'); + } }