Skip to content

Commit dc99526

Browse files
committed
fixed foreign key migration sorting issue
1 parent 06f8065 commit dc99526

File tree

5 files changed

+374
-14
lines changed

5 files changed

+374
-14
lines changed

src/Blueprint/BlueprintDiff.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
namespace Toramanlis\ImplicitMigrations\Blueprint;
44

5+
use Exception;
56
use Illuminate\Database\Schema\ColumnDefinition;
67
use Illuminate\Support\Facades\App;
78
use Illuminate\Support\Fluent;
89
use Toramanlis\ImplicitMigrations\Attributes\IndexType;
910

10-
class BlueprintDiff
11+
class BlueprintDiff implements Migratable
1112
{
1213
/**
1314
* @param SimplifyingBlueprint $from
@@ -29,11 +30,9 @@ public function __construct(
2930
public array $renamedIndexes,
3031
public array $addedIndexes
3132
) {
32-
$this->applyColumnIndexes();
33-
$this->applyColumnIndexes(true);
3433
}
3534

36-
protected function applyColumnIndexes(bool $reverse = false)
35+
public function applyColumnIndexes(bool $reverse = false)
3736
{
3837
/** @var SimplifyingBlueprint */
3938
$blueprint = App::make(SimplifyingBlueprint::class, ['tableName' => $this->to->getTable()]);
@@ -162,4 +161,45 @@ public function getRename(bool $reverse = false)
162161
$rename = [$this->from->getTable(), $this->to->getTable()];
163162
return $reverse ? array_reverse($rename) : $rename;
164163
}
164+
165+
public function getDependedColumnNames(): array
166+
{
167+
$dependedColumns = [];
168+
foreach ($this->addedIndexes as $index) {
169+
if (IndexType::Foreign->value !== $index->name) {
170+
continue;
171+
}
172+
173+
$references = is_array($index->references) ? $index->references : [$index->references];
174+
foreach ($references as $reference) {
175+
$dependedColumns[] = "{$index->on}.{$reference}";
176+
}
177+
}
178+
179+
return $dependedColumns;
180+
}
181+
182+
public function getAddedColumnNames(): array
183+
{
184+
return array_map(fn ($column) => $column->name, $this->addedColumns);
185+
}
186+
187+
public function extractForeignKey(string $on, string $reference): Fluent
188+
{
189+
foreach ($this->addedIndexes as $index) {
190+
$references = is_array($index->references) ? $index->references : [$index->references];
191+
if (
192+
IndexType::Foreign->value !== $index->name ||
193+
$index->on !== $on ||
194+
!in_array($reference, $index->references)
195+
) {
196+
continue;
197+
}
198+
199+
$this->addedIndexes = array_filter($this->addedIndexes, fn($i) => $i->index !== $index->index);
200+
return $index;
201+
}
202+
203+
throw new Exception("Reference {$on}.{$reference} has no foreign key in blueprint for {$this->to->getTable()}");
204+
}
165205
}

src/Blueprint/Migratable.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Toramanlis\ImplicitMigrations\Blueprint;
4+
5+
use Illuminate\Support\Fluent;
6+
7+
interface Migratable
8+
{
9+
public function getDependedColumnNames(): array;
10+
11+
public function getAddedColumnNames(): array;
12+
13+
public function extractForeignKey(string $on, string $reference): Fluent;
14+
}

src/Blueprint/SimplifyingBlueprint.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
namespace Toramanlis\ImplicitMigrations\Blueprint;
44

5+
use Exception;
56
use Illuminate\Database\Connection;
67
use Illuminate\Database\Schema\Blueprint;
78
use Illuminate\Support\Facades\App;
89
use Illuminate\Support\Facades\DB;
910
use Illuminate\Support\Fluent;
1011
use Toramanlis\ImplicitMigrations\Attributes\IndexType;
1112

12-
class SimplifyingBlueprint extends Blueprint
13+
class SimplifyingBlueprint extends Blueprint implements Migratable
1314
{
1415
public function __construct($tableName, $prefix = '')
1516
{
@@ -159,4 +160,46 @@ protected function dropIndexCommand($command, $type, $index)
159160
$this->commands = $remainingCommands;
160161
return $command;
161162
}
163+
164+
public function getDependedColumnNames(): array
165+
{
166+
$dependedColumnNames = [];
167+
foreach ($this->commands as $command) {
168+
if (IndexType::Foreign->value !== $command->name) {
169+
continue;
170+
}
171+
172+
$references = is_array($command->references) ? $command->references : [$command->references];
173+
foreach ($references as $reference) {
174+
$dependedColumnNames[] = "{$command->on}.{$reference}";
175+
}
176+
}
177+
178+
return $dependedColumnNames;
179+
}
180+
181+
public function getAddedColumnNames(): array
182+
{
183+
return array_map(fn ($column) => $column->name, $this->columns);
184+
}
185+
186+
public function extractForeignKey(string $on, string $reference): Fluent
187+
{
188+
foreach ($this->commands as $command) {
189+
$references = is_array($command->references) ? $command->references : [$command->references];
190+
191+
if (
192+
IndexType::Foreign->value !== $command->name ||
193+
$command->on !== $on ||
194+
!in_array($reference, $references)
195+
) {
196+
continue;
197+
}
198+
199+
$this->dropForeign($command->index);
200+
return $command;
201+
}
202+
203+
throw new Exception("Reference {$on}.{$reference} has no foreign key in blueprint for {$this->getTable()}");
204+
}
162205
}

src/Generator/MigrationGenerator.php

Lines changed: 172 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
namespace Toramanlis\ImplicitMigrations\Generator;
44

55
use Illuminate\Database\Schema\Blueprint;
6-
use Toramanlis\ImplicitMigrations\Blueprint\BlueprintDiff;
76
use Toramanlis\ImplicitMigrations\Blueprint\Exporters\TableDiffExporter;
87
use Toramanlis\ImplicitMigrations\Blueprint\Exporters\TableExporter;
98
use Toramanlis\ImplicitMigrations\Blueprint\Manager;
109
use Toramanlis\ImplicitMigrations\Blueprint\SimplifyingBlueprint;
1110
use Illuminate\Database\Migrations\Migration;
1211
use Illuminate\Support\Facades\App;
12+
use Toramanlis\ImplicitMigrations\Blueprint\BlueprintDiff;
13+
use Toramanlis\ImplicitMigrations\Blueprint\Migratable;
1314

1415
/** @package Toramanlis\ImplicitMigrations\Generator */
1516
class MigrationGenerator
@@ -73,11 +74,12 @@ public function generate(array $modelNames): array
7374
$sourceMap[$tableName] = $sourceMap[$tableName] ?? $relationship->getSource();
7475
}
7576

77+
$migratables = [];
7678
foreach ($blueprintManager->getBlueprints() as $table => $blueprint) {
7779
$blueprint->removeDuplicatePrimaries();
7880
$source = $sourceMap[$table];
7981
if (!isset($this->existingBlueprints[$source])) {
80-
$migrationData[$table] = $this->getMigrationItem($source, $blueprint);
82+
$migratables[$table] = $blueprint;
8183
continue;
8284
}
8385

@@ -87,22 +89,183 @@ public function generate(array $modelNames): array
8789
continue;
8890
}
8991

90-
$migrationData[$this->existingBlueprints[$source]->getTable()] = $this->getMigrationItem(
91-
$source,
92-
$diff,
93-
MigrationMode::Update
94-
);
92+
$migratables[$table] = $diff;
93+
}
94+
95+
$migratables = $this->sortMigrations($migratables);
96+
97+
foreach ($migratables as $table => $migratable) {
98+
$source = $sourceMap[$table];
99+
100+
if ($migratable instanceof SimplifyingBlueprint) {
101+
$migrationData[$table] = $this->getMigrationItem($source, $migratable);
102+
} else {
103+
/** @var BlueprintDiff $migratable */
104+
$migratable->applyColumnIndexes();
105+
$migratable->applyColumnIndexes(true);
106+
$migrationData[$this->existingBlueprints[$source]->getTable()] = $this->getMigrationItem(
107+
$source,
108+
$migratable,
109+
);
110+
}
95111
}
96112

97113
return $migrationData;
98114
}
99115

116+
/**
117+
* @param array<Migratable> $migratables
118+
* @return array<Migratable> $migratables
119+
*/
120+
protected function sortMigrations(array $migratables): array
121+
{
122+
$extraMigratables = $this->separateCodependents($migratables);
123+
124+
$sorted = [];
125+
126+
while (count($migratables)) {
127+
$dependencyMap = $this->getDependencyMap($migratables);
128+
129+
foreach (array_keys($migratables) as $table) {
130+
$dependencies = $dependencyMap[$table] ?? [];
131+
132+
$counts = array_map(fn ($item) => count($item), $dependencies);
133+
if (0 !== array_sum($counts)) {
134+
continue;
135+
}
136+
137+
$sorted[$table] = $migratables[$table];
138+
unset($migratables[$table]);
139+
}
140+
}
141+
142+
return array_merge($sorted, $extraMigratables);
143+
}
144+
145+
protected function getDependencyMap($migratables)
146+
{
147+
$addedColumns = [];
148+
149+
foreach ($migratables as $table => $migratable) {
150+
foreach ($migratable->getAddedColumnNames() as $addedColumn) {
151+
$addedColumns["{$table}.{$addedColumn}"] = $table;
152+
}
153+
}
154+
155+
$dependencyMap = [];
156+
foreach (array_keys($migratables) as $table) {
157+
$dependencyMap[$table] = $this->getDependencies($table, $migratables, $addedColumns);
158+
}
159+
160+
return $dependencyMap;
161+
}
162+
163+
164+
protected function separateCodependents($migratables): array
165+
{
166+
$extraMigratables = [];
167+
168+
while (count($codependents = $this->getCodependents($migratables))) {
169+
$biggestDependent = null;
170+
$mostDependencies = 0;
171+
172+
foreach ($codependents as $table => $dependencies) {
173+
foreach ($dependencies as $column => $columnDependencies) {
174+
if (count($columnDependencies) <= $mostDependencies) {
175+
continue;
176+
}
177+
178+
$mostDependencies = count($columnDependencies);
179+
$biggestDependent = ['table' => $table, 'column' => $column];
180+
}
181+
}
182+
183+
[$on, $shortColumn] = explode('.', $biggestDependent['column']);
184+
$extraMigratable = App::make(BlueprintDiff::class, [
185+
'from' => App::make(SimplifyingBlueprint::class, ['tableName' => $biggestDependent['table']]),
186+
'to' => App::make(SimplifyingBlueprint::class, ['tableName' => $biggestDependent['table']]),
187+
'modifiedColumns' => [],
188+
'droppedColumns' => [],
189+
'addedColumns' => [],
190+
'droppedIndexes' => [],
191+
'renamedIndexes' => [],
192+
'addedIndexes' => [],
193+
'addedIndexes' => [$migratables[$biggestDependent['table']]->extractForeignKey($on, $shortColumn)],
194+
]);
195+
$extraMigratables['_' . $biggestDependent['table']] = $extraMigratable;
196+
}
197+
198+
199+
return $extraMigratables;
200+
}
201+
202+
protected function getCodependents($migratables)
203+
{
204+
$dependencyMap = $this->getDependencyMap($migratables);
205+
206+
$codependents = [];
207+
foreach ($dependencyMap as $table => $dependencies) {
208+
foreach ($dependencies as $dependedColumn => $columnDependencies) {
209+
foreach ($columnDependencies as $dependency) {
210+
foreach ($dependencyMap[$dependency] as $counterColumn => $subdependencies) {
211+
if (!in_array($table, $subdependencies)) {
212+
continue;
213+
}
214+
215+
$codependents[$table] ??= [];
216+
$codependents[$table][$dependedColumn] ??= [];
217+
if (!in_array($dependency, $codependents[$table][$dependedColumn])) {
218+
$codependents[$table][$dependedColumn][] = $dependency;
219+
}
220+
221+
$codependents[$dependency] ??= [];
222+
$codependents[$dependency][$counterColumn] ??= [];
223+
if (!in_array($table, $codependents[$dependency][$counterColumn])) {
224+
$codependents[$dependency][$counterColumn][] = $table;
225+
}
226+
}
227+
}
228+
}
229+
}
230+
231+
return $codependents;
232+
}
233+
234+
protected function getDependencies($table, $migratables, $addedColumns, $chain = []): array
235+
{
236+
if (in_array($table, $chain)) {
237+
return [];
238+
}
239+
240+
$chain = array_merge($chain, [$table]);
241+
242+
$dependencies = [];
243+
$migratable = $migratables[$table];
244+
foreach ($migratable->getDependedColumnNames() as $dependedColumn) {
245+
if (!isset($addedColumns[$dependedColumn])) {
246+
continue;
247+
}
248+
249+
$dependency = $addedColumns[$dependedColumn];
250+
$subDependencies = $this->getDependencies($dependency, $migratables, $addedColumns, $chain);
251+
$merged = array_unique(array_merge([$dependency], $subDependencies));
252+
253+
if (1 === count($chain)) {
254+
$dependencies[$dependedColumn] = $merged;
255+
} else {
256+
$dependencies = $merged;
257+
}
258+
}
259+
260+
return $dependencies;
261+
}
262+
100263
protected function getMigrationItem(
101264
string $source,
102-
Blueprint|BlueprintDiff $definition,
103-
MigrationMode $mode = MigrationMode::Create
265+
Migratable $definition,
104266
) {
105267
$modelName = explode('::', $source)[0];
268+
$mode = $definition instanceof SimplifyingBlueprint ? MigrationMode::Create : MigrationMode::Update;
106269

107270
$exporter = match ($mode) {
108271
MigrationMode::Create => TableExporter::class,

0 commit comments

Comments
 (0)