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

Port changes from cakephp/phinx#2322 #789

Merged
merged 1 commit into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/writing-migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Db/Adapter/PostgresAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
58 changes: 57 additions & 1 deletion src/Db/Table/ForeignKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
*/
protected static array $validOptions = ['delete', 'update', 'constraint'];
protected static array $validOptions = ['delete', 'update', 'constraint', 'deferrable'];

/**
* @var string[]
Expand Down Expand Up @@ -53,6 +56,11 @@ class ForeignKey
*/
protected ?string $constraint = null;

/**
* @var string|null
*/
protected ?string $deferrableMode = null;

/**
* Sets the foreign key columns.
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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);
Expand All @@ -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);
}
}
37 changes: 37 additions & 0 deletions tests/TestCase/Db/Table/ForeignKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Loading