Skip to content

Commit 2f52459

Browse files
authored
Auto-generate foreign key constraint names when not provided (#1041)
* Auto-generate foreign key constraint names when not provided When a foreign key is added without an explicit name, the MysqlAdapter and SqliteAdapter now generate a name following the pattern 'tablename_columnname' (e.g., 'articles_user_id' for a FK on the user_id column in the articles table). This matches the behavior of PostgresAdapter and SqlserverAdapter, which already auto-generate FK names. This ensures constraint names are always strings and prevents issues with constraint lookup methods that expect string names. * Update test comparison files for new FK naming convention Update the expected FK names in comparison files and schema dumps to match the new auto-generated naming pattern (tablename_columnname). * Update test comparison files with index rename operations The FK constraint name change from auto-generated (ibfk_N) to explicit (table_column) also affects the implicit index MySQL creates for FKs. Update comparison files to reflect the index rename from user_id to articles_user_id. * Remove unnecessary index operations from comparison file With the FK naming changes, both the lock file and database have the index named articles_user_id, so no diff is needed for this index. Remove the index rename operations that were incorrectly added. * Add conflict resolution for auto-generated FK constraint names (#1042) * Add conflict resolution for auto-generated FK constraint names When auto-generating FK constraint names, check if the name already exists and append a counter suffix (_2, _3, etc.) if needed. This prevents duplicate constraint name errors when multiple FKs are created on the same columns with different references. * Remove unused variable * Truncate FK constraint names to max 128 characters Limit auto-generated foreign key constraint names to 125 characters to ensure the final name (including potential _XX counter suffix) stays within 128 characters. This prevents identifier length errors on databases with strict limits (MySQL: 64, PostgreSQL: 63). * Use database-specific identifier length limits - MySQL: 61 chars (64 limit - 3 for _XX suffix) - PostgreSQL: 60 chars (63 limit - 3 for _XX suffix) - SQL Server: 125 chars (128 limit - 3 for _XX suffix) - SQLite: No limit needed * Use IDENTIFIER_MAX_LENGTH class constant for clarity Each adapter now defines its database-specific identifier length limit as a class constant, making the code more self-documenting.
2 parents 273b420 + 2454828 commit 2f52459

File tree

7 files changed

+145
-23
lines changed

7 files changed

+145
-23
lines changed

src/Db/Adapter/MysqlAdapter.php

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
*/
3030
class MysqlAdapter extends AbstractAdapter
3131
{
32+
/**
33+
* Maximum length for identifiers (table names, column names, constraint names, etc.)
34+
*/
35+
protected const IDENTIFIER_MAX_LENGTH = 64;
36+
3237
/**
3338
* @var string[]
3439
*/
@@ -977,7 +982,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
977982
{
978983
$alter = sprintf(
979984
'ADD %s',
980-
$this->getForeignKeySqlDefinition($foreignKey),
985+
$this->getForeignKeySqlDefinition($foreignKey, $table->getName()),
981986
);
982987

983988
return new AlterInstructions([$alter]);
@@ -1192,14 +1197,13 @@ protected function getIndexSqlDefinition(Index $index): string
11921197
* Gets the MySQL Foreign Key Definition for an ForeignKey object.
11931198
*
11941199
* @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key
1200+
* @param string $tableName Table name for auto-generating constraint name
11951201
* @return string
11961202
*/
1197-
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
1203+
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
11981204
{
1199-
$def = '';
1200-
if ($foreignKey->getName()) {
1201-
$def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName());
1202-
}
1205+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
1206+
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
12031207
$columnNames = [];
12041208
foreach ($foreignKey->getColumns() as $column) {
12051209
$columnNames[] = $this->quoteColumnName($column);
@@ -1222,6 +1226,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
12221226
return $def;
12231227
}
12241228

1229+
/**
1230+
* Generate a unique foreign key constraint name.
1231+
*
1232+
* @param string $tableName Table name
1233+
* @param array<string> $columns Column names
1234+
* @return string
1235+
*/
1236+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
1237+
{
1238+
$baseName = $tableName . '_' . implode('_', $columns);
1239+
$maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
1240+
if (strlen($baseName) > $maxLength) {
1241+
$baseName = substr($baseName, 0, $maxLength);
1242+
}
1243+
$existingKeys = $this->getForeignKeys($tableName);
1244+
$existingNames = array_column($existingKeys, 'name');
1245+
1246+
if (!in_array($baseName, $existingNames, true)) {
1247+
return $baseName;
1248+
}
1249+
1250+
$counter = 2;
1251+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
1252+
$counter++;
1253+
}
1254+
1255+
return $baseName . '_' . $counter;
1256+
}
1257+
12251258
/**
12261259
* Returns MySQL column types (inherited and MySQL specified).
12271260
*

src/Db/Adapter/PostgresAdapter.php

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727

2828
class PostgresAdapter extends AbstractAdapter
2929
{
30+
/**
31+
* Maximum length for identifiers (table names, column names, constraint names, etc.)
32+
*/
33+
protected const IDENTIFIER_MAX_LENGTH = 63;
34+
3035
public const GENERATED_ALWAYS = 'ALWAYS';
3136
public const GENERATED_BY_DEFAULT = 'BY DEFAULT';
3237
/**
@@ -949,11 +954,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
949954
*/
950955
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
951956
{
952-
$parts = $this->getSchemaName($tableName);
953-
954-
$constraintName = $foreignKey->getName() ?: (
955-
$parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey'
956-
);
957+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
957958
$columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns()));
958959
$refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns()));
959960
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) .
@@ -972,6 +973,36 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta
972973
return $def;
973974
}
974975

976+
/**
977+
* Generate a unique foreign key constraint name.
978+
*
979+
* @param string $tableName Table name
980+
* @param array<string> $columns Column names
981+
* @return string
982+
*/
983+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
984+
{
985+
$parts = $this->getSchemaName($tableName);
986+
$baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey';
987+
$maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
988+
if (strlen($baseName) > $maxLength) {
989+
$baseName = substr($baseName, 0, $maxLength);
990+
}
991+
$existingKeys = $this->getForeignKeys($tableName);
992+
$existingNames = array_column($existingKeys, 'name');
993+
994+
if (!in_array($baseName, $existingNames, true)) {
995+
return $baseName;
996+
}
997+
998+
$counter = 2;
999+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
1000+
$counter++;
1001+
}
1002+
1003+
return $baseName . '_' . $counter;
1004+
}
1005+
9751006
/**
9761007
* @inheritDoc
9771008
*/

src/Db/Adapter/SqliteAdapter.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,7 +1425,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
14251425
$tableName = $table->getName();
14261426
$instructions->addPostStep(function ($state) use ($foreignKey, $tableName) {
14271427
$this->execute('pragma foreign_keys = ON');
1428-
$sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); ';
1428+
$sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey, $tableName) . '); ';
14291429

14301430
//Delete indexes from original table and recreate them in temporary table
14311431
$schema = $this->getSchemaName($tableName, true)['schema'];
@@ -1670,14 +1670,13 @@ public function getColumnTypes(): array
16701670
* Gets the SQLite Foreign Key Definition for an ForeignKey object.
16711671
*
16721672
* @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key
1673+
* @param string $tableName Table name for auto-generating constraint name
16731674
* @return string
16741675
*/
1675-
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
1676+
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
16761677
{
1677-
$def = '';
1678-
if ($foreignKey->getName()) {
1679-
$def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName());
1680-
}
1678+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
1679+
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
16811680
$columnNames = [];
16821681
foreach ($foreignKey->getColumns() as $column) {
16831682
$columnNames[] = $this->quoteColumnName($column);
@@ -1698,6 +1697,31 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
16981697
return $def;
16991698
}
17001699

1700+
/**
1701+
* Generate a unique foreign key constraint name.
1702+
*
1703+
* @param string $tableName Table name
1704+
* @param array<string> $columns Column names
1705+
* @return string
1706+
*/
1707+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
1708+
{
1709+
$baseName = $tableName . '_' . implode('_', $columns);
1710+
$existingKeys = $this->getForeignKeys($tableName);
1711+
$existingNames = array_column($existingKeys, 'name');
1712+
1713+
if (!in_array($baseName, $existingNames, true)) {
1714+
return $baseName;
1715+
}
1716+
1717+
$counter = 2;
1718+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
1719+
$counter++;
1720+
}
1721+
1722+
return $baseName . '_' . $counter;
1723+
}
1724+
17011725
/**
17021726
* @inheritDoc
17031727
*/

src/Db/Adapter/SqlserverAdapter.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
*/
2929
class SqlserverAdapter extends AbstractAdapter
3030
{
31+
/**
32+
* Maximum length for identifiers (table names, column names, constraint names, etc.)
33+
*/
34+
protected const IDENTIFIER_MAX_LENGTH = 128;
35+
3136
/**
3237
* @var string[]
3338
*/
@@ -864,7 +869,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
864869
*/
865870
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
866871
{
867-
$constraintName = $foreignKey->getName() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
872+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
868873
$columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns()));
869874
$refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns()));
870875

@@ -881,6 +886,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta
881886
return $def;
882887
}
883888

889+
/**
890+
* Generate a unique foreign key constraint name.
891+
*
892+
* @param string $tableName Table name
893+
* @param array<string> $columns Column names
894+
* @return string
895+
*/
896+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
897+
{
898+
$baseName = $tableName . '_' . implode('_', $columns);
899+
$maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
900+
if (strlen($baseName) > $maxLength) {
901+
$baseName = substr($baseName, 0, $maxLength);
902+
}
903+
$existingKeys = $this->getForeignKeys($tableName);
904+
$existingNames = array_column($existingKeys, 'name');
905+
906+
if (!in_array($baseName, $existingNames, true)) {
907+
return $baseName;
908+
}
909+
910+
$counter = 2;
911+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
912+
$counter++;
913+
}
914+
915+
return $baseName . '_' . $counter;
916+
}
917+
884918
/**
885919
* Creates the specified schema.
886920
*
Binary file not shown.

tests/comparisons/Diff/default/the_diff_default_mysql.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class TheDiffDefaultMysql extends BaseMigration
1616
public function up(): void
1717
{
1818
$this->table('articles')
19-
->dropForeignKey([], 'articles_ibfk_1')
19+
->dropForeignKey([], 'articles_user_id')
2020
->removeIndexByName('UNIQUE_SLUG')
2121
->removeIndexByName('rating_index')
2222
->removeIndexByName('BY_NAME')
@@ -86,7 +86,7 @@ public function up(): void
8686
])
8787
->addIndex(
8888
$this->index('user_id')
89-
->setName('categories_ibfk_1')
89+
->setName('categories_user_id')
9090
)
9191
->addIndex(
9292
$this->index('name')
@@ -101,7 +101,7 @@ public function up(): void
101101
->setReferencedColumns('id')
102102
->setDelete('RESTRICT')
103103
->setUpdate('RESTRICT')
104-
->setName('categories_ibfk_1')
104+
->setName('categories_user_id')
105105
)
106106
->update();
107107

@@ -234,7 +234,7 @@ public function down(): void
234234
->setReferencedColumns('id')
235235
->setDelete('CASCADE')
236236
->setUpdate('CASCADE')
237-
->setName('articles_ibfk_1')
237+
->setName('articles_user_id')
238238
)
239239
->update();
240240

tests/comparisons/Diff/simple/the_diff_simple_mysql.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function up(): void
7272
->setReferencedColumns('id')
7373
->setDelete('RESTRICT')
7474
->setUpdate('RESTRICT')
75-
->setName('articles_ibfk_1')
75+
->setName('articles_user_id')
7676
)
7777
->update();
7878
}

0 commit comments

Comments
 (0)