Created
June 22, 2023 18:49
-
-
Save godismyjudge95/12c3cebeb3156c84fbac795a8f803382 to your computer and use it in GitHub Desktop.
A Laravel migration to convert the primary keys from int to bigint
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
use Illuminate\Database\Migrations\Migration; | |
use Doctrine\DBAL\Schema\AbstractSchemaManager as DoctrineSchemaManager; | |
use Doctrine\DBAL\Types\IntegerType; | |
use Illuminate\Database\Schema\Blueprint; | |
use Illuminate\Database\Schema\Builder as SchemaBuilder; | |
use Symfony\Component\Console\Output\ConsoleOutput; | |
/** | |
* A Laravel migration to convert the primary keys from int to bigint | |
* Heavily borrowed code from: https://github.com/AXN-Informatique/laravel-pk-int-to-bigint | |
*/ | |
return new class extends Migration | |
{ | |
protected DoctrineSchemaManager $doctrineSchemaManager; | |
protected SchemaBuilder $schemaBuilder; | |
private array $intColumnsInfo = []; | |
private array $foreignKeysConstraintsInfo = []; | |
public function up(): void | |
{ | |
$this->connection = Schema::getConnection(); | |
$this->doctrineSchemaManager = DB::getDoctrineSchemaManager(); | |
$this->schemaBuilder = DB::getSchemaBuilder(); | |
DB::getDoctrineConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); | |
$this->transform(); | |
} | |
/** | |
* 1) Drop all foreign key constraints on each table | |
* 2) Change INT to BIGINT on primary and foreign key columns on each table | |
* 3) Restore all foreign key constraints on each table | |
* | |
* @return void | |
*/ | |
public function transform(): void | |
{ | |
$this->extractSchemaInfos(); | |
$hasConstraintAnomaly = false; | |
foreach ($this->foreignKeysConstraintsInfo as $constraint) { | |
// If there are data that do not respect a foreign key constraint, | |
// it will be impossible to restore the constraint after deleting it. | |
// So, we check this before doing any action. | |
if ($this->hasConstraintAnomaly($constraint)) { | |
$this->message( | |
"Foreign key constraint anomaly: [{$constraint['name']}] " | |
."{$constraint['table']}.{$constraint['column']} references " | |
."{$constraint['relatedTable']}.{$constraint['relatedColumn']}", | |
'error' | |
); | |
$hasConstraintAnomaly = true; | |
} | |
} | |
if ($hasConstraintAnomaly) { | |
return; | |
} | |
// DROP FOREIGN KEY CONSTRAINTS | |
foreach ($this->foreignKeysConstraintsInfo as $constraint) { | |
$this->message("Drop foreign on {$constraint['table']}.{$constraint['column']}"); | |
$this->schemaBuilder->table($constraint['table'], function (Blueprint $blueprint) use ($constraint) { | |
$blueprint->dropForeign($constraint['name']); | |
}); | |
} | |
// CHANGE INT TO BIGINT | |
foreach ($this->intColumnsInfo as $column) { | |
$this->message("Change INT to BIGINT for {$column['table']}.{$column['column']}"); | |
$this->schemaBuilder->table($column['table'], function (Blueprint $blueprint) use ($column) { | |
$blueprint | |
->unsignedBigInteger($column['column'], $column['autoIncrement']) | |
->nullable($column['nullable']) | |
->default($column['default']) | |
->change(); | |
}); | |
} | |
// RESTORE FOREIGN KEY CONSTRAINTS | |
foreach ($this->foreignKeysConstraintsInfo as $constraint) { | |
$this->message("Restore foreign on {$constraint['table']}.{$constraint['column']}"); | |
$this->schemaBuilder->table($constraint['table'], function (Blueprint $blueprint) use ($constraint) { | |
$blueprint | |
->foreign($constraint['column'], $constraint['name']) | |
->references($constraint['relatedColumn']) | |
->on($constraint['relatedTable']) | |
->onDelete($constraint['onDelete']) | |
->onUpdate($constraint['onUpdate']); | |
}); | |
} | |
} | |
/** | |
* On each table : | |
* 1) Extract information on unsigned integer columns that are primary or foreign key. | |
* 2) Extract information on foreign keys constraints concerning unsigned integer columns. | |
* | |
* @return void | |
*/ | |
private function extractSchemaInfos(): void | |
{ | |
$this->intColumnsInfo = []; | |
$this->foreignKeysConstraintsInfo = []; | |
foreach ($this->doctrineSchemaManager->listTables() as $table) { | |
$tableIntColumnsNames = []; | |
// GET TABLE KEYS COLUMNS NAMES | |
$tableKeysColumnsNames = []; | |
// primary keys... | |
if ($primaryKey = $table->getPrimaryKey()) { | |
$tableKeysColumnsNames = $primaryKey->getColumns(); | |
} | |
// ... + foreign keys | |
foreach ($table->getForeignKeys() as $foreignKey) { | |
$tableKeysColumnsNames = array_merge($tableKeysColumnsNames, $foreignKey->getLocalColumns()); | |
} | |
// GET UNSIGNED INTEGER COLUMNS NAMES AND INFOS | |
foreach ($table->getColumns() as $column) { | |
// keep only unsigned integer columns that are a key | |
if (! $column->getType() instanceof IntegerType | |
|| ! $column->getUnsigned() | |
|| ! in_array($column->getName(), $tableKeysColumnsNames)) { | |
continue; | |
} | |
$tableIntColumnsNames[] = $column->getName(); | |
$this->intColumnsInfo[] = [ | |
'table' => $table->getName(), | |
'column' => $column->getName(), | |
'nullable' => ! $column->getNotnull(), | |
'default' => $column->getDefault(), | |
'autoIncrement' => $column->getAutoincrement(), | |
]; | |
} | |
// GET FOREIGN KEYS CONSTRAINTS INFOS | |
foreach ($table->getForeignKeys() as $foreignKey) { | |
// keep only foreign keys that are unsigned integer | |
if (! in_array($foreignKey->getLocalColumns()[0], $tableIntColumnsNames)) { | |
continue; | |
} | |
$this->foreignKeysConstraintsInfo[] = [ | |
'name' => $foreignKey->getName(), | |
'table' => $foreignKey->getLocalTableName(), | |
'column' => $foreignKey->getLocalColumns()[0], | |
'relatedTable' => $foreignKey->getForeignTableName(), | |
'relatedColumn' => $foreignKey->getForeignColumns()[0], | |
'onUpdate' => $foreignKey->onUpdate(), | |
'onDelete' => $foreignKey->onDelete(), | |
]; | |
} | |
} | |
} | |
/** | |
* Says if there are data that do not respect a foreign key constraint. | |
* | |
* @param array $constraint | |
* @return bool | |
*/ | |
private function hasConstraintAnomaly(array $constraint): bool | |
{ | |
return $this->connection | |
->table($constraint['table']) | |
->whereNotNull($constraint['column']) | |
->whereNotIn($constraint['column'], function ($query) use ($constraint) { | |
$query | |
->from($constraint['relatedTable']) | |
->select($constraint['relatedColumn']); | |
}) | |
->exists(); | |
} | |
/** | |
* Print message to the console if set, or do an echo. | |
* | |
* @param string $message | |
* @param string $style | |
* @return void | |
*/ | |
protected function message(string $message, string $style = 'info'): void | |
{ | |
$output = new ConsoleOutput(); | |
$styled = $style ? "<$style>$message</$style>" : $message; | |
$output->writeln($styled); | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment