Skip to content

Commit 1a87b9e

Browse files
committed
Perform change_foreign_key in two steps required for some databases
1 parent 467753a commit 1a87b9e

5 files changed

Lines changed: 39 additions & 25 deletions

File tree

docs/database-migrations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ return [
483483

484484
## Modify Foreign Key
485485

486-
Modifies an existing foreign key constraint (e.g. change referenced table, `ON DELETE`/`ON UPDATE` behaviour). Implemented as drop then add with new options.
486+
Modifies an existing foreign key constraint (e.g. change referenced table, `ON DELETE`/`ON UPDATE` behaviour). Executed as two separate statements: first DROP FOREIGN KEY, then ADD CONSTRAINT (databases do not allow drop and add in a single ALTER TABLE).
487487

488488
### Fields
489489
- `type` (required): Must be "modify_foreign_key"

src/Migration/Action/ActionHandler.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public function getType(): string;
2525

2626
/**
2727
* @param array<string, mixed> $action
28+
*
29+
* @return array<MigrationStep>|MigrationStep
2830
*/
29-
public function buildStep(array $action): MigrationStep;
31+
public function buildStep(array $action): array|MigrationStep;
3032
}

src/Migration/Action/ModifyForeignKeyActionHandler.php

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ public function getType(): string
2121
return 'modify_foreign_key';
2222
}
2323

24-
public function buildStep(array $action): MigrationStep
24+
/**
25+
* @return array<MigrationStep>
26+
*/
27+
public function buildStep(array $action): array
2528
{
2629
$tableName = $this->requireTableName($action);
2730

@@ -49,8 +52,15 @@ public function buildStep(array $action): MigrationStep
4952
$onDelete = isset($action['on_delete']) ? 'ON DELETE '.$action['on_delete'] : '';
5053
$onUpdate = isset($action['on_update']) ? 'ON UPDATE '.$action['on_update'] : '';
5154

52-
$addPart = sprintf(
53-
'ADD CONSTRAINT `%s` FOREIGN KEY (`%s`) REFERENCES `%s` (`%s`) %s %s',
55+
$dropQuery = sprintf(
56+
"ALTER TABLE `%s`\n DROP FOREIGN KEY `%s`",
57+
$tableName,
58+
$constraintName
59+
);
60+
61+
$addQuery = sprintf(
62+
"ALTER TABLE `%s`\n ADD CONSTRAINT `%s` FOREIGN KEY (`%s`) REFERENCES `%s` (`%s`) %s %s",
63+
$tableName,
5464
$constraintName,
5565
$action['column'],
5666
$action['foreign_table'],
@@ -59,13 +69,9 @@ public function buildStep(array $action): MigrationStep
5969
$onUpdate
6070
);
6171

62-
$query = sprintf(
63-
"ALTER TABLE `%s`\n DROP FOREIGN KEY `%s`,\n %s",
64-
$tableName,
65-
$constraintName,
66-
trim($addPart)
67-
);
68-
69-
return new QueryStep($query, []);
72+
return [
73+
new QueryStep($dropQuery, []),
74+
new QueryStep(trim($addQuery), []),
75+
];
7076
}
7177
}

src/Migration/DatabaseManager.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ public function execute(array $data, bool $dryRun = false): void
8989
}
9090

9191
$handler = $this->getHandler($action['type']);
92-
$steps[] = $handler->buildStep($action);
92+
$stepOrSteps = $handler->buildStep($action);
93+
$steps = array_merge($steps, is_array($stepOrSteps) ? $stepOrSteps : [$stepOrSteps]);
9394
}
9495

9596
if ($dryRun)

tests/Unit/Migration/Action/ModifyForeignKeyActionHandlerTest.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function testGetType()
2020
verify($handler->getType())->equals('modify_foreign_key');
2121
}
2222

23-
public function testBuildStepWithValidAction()
23+
public function testBuildStepWithValidActionReturnsTwoSteps()
2424
{
2525
$handler = new ModifyForeignKeyActionHandler();
2626
$action = [
@@ -33,16 +33,21 @@ public function testBuildStepWithValidAction()
3333
'on_update' => 'CASCADE',
3434
];
3535

36-
$step = $handler->buildStep($action);
37-
verify($step)->instanceOf(QueryStep::class);
38-
verify($step->getQuery())->stringContainsString('ALTER TABLE `orders`');
39-
verify($step->getQuery())->stringContainsString('DROP FOREIGN KEY `orders_fk_user_id`');
40-
verify($step->getQuery())->stringContainsString('ADD CONSTRAINT `orders_fk_user_id`');
41-
verify($step->getQuery())->stringContainsString('FOREIGN KEY (`user_id`)');
42-
verify($step->getQuery())->stringContainsString('REFERENCES `users` (`id`)');
43-
verify($step->getQuery())->stringContainsString('ON DELETE SET NULL');
44-
verify($step->getQuery())->stringContainsString('ON UPDATE CASCADE');
45-
verify($step->getParams())->equals([]);
36+
$steps = $handler->buildStep($action);
37+
verify($steps)->isArray();
38+
verify($steps)->arrayCount(2);
39+
verify($steps[0])->instanceOf(QueryStep::class);
40+
verify($steps[1])->instanceOf(QueryStep::class);
41+
verify($steps[0]->getQuery())->stringContainsString('ALTER TABLE `orders`');
42+
verify($steps[0]->getQuery())->stringContainsString('DROP FOREIGN KEY `orders_fk_user_id`');
43+
verify($steps[1]->getQuery())->stringContainsString('ALTER TABLE `orders`');
44+
verify($steps[1]->getQuery())->stringContainsString('ADD CONSTRAINT `orders_fk_user_id`');
45+
verify($steps[1]->getQuery())->stringContainsString('FOREIGN KEY (`user_id`)');
46+
verify($steps[1]->getQuery())->stringContainsString('REFERENCES `users` (`id`)');
47+
verify($steps[1]->getQuery())->stringContainsString('ON DELETE SET NULL');
48+
verify($steps[1]->getQuery())->stringContainsString('ON UPDATE CASCADE');
49+
verify($steps[0]->getParams())->equals([]);
50+
verify($steps[1]->getParams())->equals([]);
4651
}
4752

4853
public function testBuildStepThrowsExceptionWhenTableNameMissing()

0 commit comments

Comments
 (0)