diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index a8aa1e8c..694f821b 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -29,6 +29,11 @@ */ class MysqlAdapter extends AbstractAdapter { + /** + * Maximum length for identifiers (table names, column names, constraint names, etc.) + */ + protected const IDENTIFIER_MAX_LENGTH = 64; + /** * @var string[] */ @@ -977,7 +982,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey { $alter = sprintf( 'ADD %s', - $this->getForeignKeySqlDefinition($foreignKey), + $this->getForeignKeySqlDefinition($foreignKey, $table->getName()), ); return new AlterInstructions([$alter]); @@ -1192,14 +1197,13 @@ protected function getIndexSqlDefinition(Index $index): string * Gets the MySQL Foreign Key Definition for an ForeignKey object. * * @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key + * @param string $tableName Table name for auto-generating constraint name * @return string */ - protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $def = ''; - if ($foreignKey->getName()) { - $def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName()); - } + $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); + $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $columnNames = []; foreach ($foreignKey->getColumns() as $column) { $columnNames[] = $this->quoteColumnName($column); @@ -1222,6 +1226,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string return $def; } + /** + * Generate a unique foreign key constraint name. + * + * @param string $tableName Table name + * @param array $columns Column names + * @return string + */ + protected function getUniqueForeignKeyName(string $tableName, array $columns): string + { + $baseName = $tableName . '_' . implode('_', $columns); + $maxLength = static::IDENTIFIER_MAX_LENGTH - 3; + if (strlen($baseName) > $maxLength) { + $baseName = substr($baseName, 0, $maxLength); + } + $existingKeys = $this->getForeignKeys($tableName); + $existingNames = array_column($existingKeys, 'name'); + + if (!in_array($baseName, $existingNames, true)) { + return $baseName; + } + + $counter = 2; + while (in_array($baseName . '_' . $counter, $existingNames, true)) { + $counter++; + } + + return $baseName . '_' . $counter; + } + /** * Returns MySQL column types (inherited and MySQL specified). * diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 878f81ee..b0e61d22 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -27,6 +27,11 @@ class PostgresAdapter extends AbstractAdapter { + /** + * Maximum length for identifiers (table names, column names, constraint names, etc.) + */ + protected const IDENTIFIER_MAX_LENGTH = 63; + public const GENERATED_ALWAYS = 'ALWAYS'; public const GENERATED_BY_DEFAULT = 'BY DEFAULT'; /** @@ -949,11 +954,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $parts = $this->getSchemaName($tableName); - - $constraintName = $foreignKey->getName() ?: ( - $parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey' - ); + $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); $columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns())); $refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns())); $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) . @@ -972,6 +973,36 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta return $def; } + /** + * Generate a unique foreign key constraint name. + * + * @param string $tableName Table name + * @param array $columns Column names + * @return string + */ + protected function getUniqueForeignKeyName(string $tableName, array $columns): string + { + $parts = $this->getSchemaName($tableName); + $baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey'; + $maxLength = static::IDENTIFIER_MAX_LENGTH - 3; + if (strlen($baseName) > $maxLength) { + $baseName = substr($baseName, 0, $maxLength); + } + $existingKeys = $this->getForeignKeys($tableName); + $existingNames = array_column($existingKeys, 'name'); + + if (!in_array($baseName, $existingNames, true)) { + return $baseName; + } + + $counter = 2; + while (in_array($baseName . '_' . $counter, $existingNames, true)) { + $counter++; + } + + return $baseName . '_' . $counter; + } + /** * @inheritDoc */ diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 9145e0cb..82a3812d 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -1425,7 +1425,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey $tableName = $table->getName(); $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) { $this->execute('pragma foreign_keys = ON'); - $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey) . '); '; + $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey, $tableName) . '); '; //Delete indexes from original table and recreate them in temporary table $schema = $this->getSchemaName($tableName, true)['schema']; @@ -1670,14 +1670,13 @@ public function getColumnTypes(): array * Gets the SQLite Foreign Key Definition for an ForeignKey object. * * @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key + * @param string $tableName Table name for auto-generating constraint name * @return string */ - protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string + protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $def = ''; - if ($foreignKey->getName()) { - $def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName()); - } + $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); + $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $columnNames = []; foreach ($foreignKey->getColumns() as $column) { $columnNames[] = $this->quoteColumnName($column); @@ -1698,6 +1697,31 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string return $def; } + /** + * Generate a unique foreign key constraint name. + * + * @param string $tableName Table name + * @param array $columns Column names + * @return string + */ + protected function getUniqueForeignKeyName(string $tableName, array $columns): string + { + $baseName = $tableName . '_' . implode('_', $columns); + $existingKeys = $this->getForeignKeys($tableName); + $existingNames = array_column($existingKeys, 'name'); + + if (!in_array($baseName, $existingNames, true)) { + return $baseName; + } + + $counter = 2; + while (in_array($baseName . '_' . $counter, $existingNames, true)) { + $counter++; + } + + return $baseName . '_' . $counter; + } + /** * @inheritDoc */ diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index e804d590..67f08c00 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -28,6 +28,11 @@ */ class SqlserverAdapter extends AbstractAdapter { + /** + * Maximum length for identifiers (table names, column names, constraint names, etc.) + */ + protected const IDENTIFIER_MAX_LENGTH = 128; + /** * @var string[] */ @@ -864,7 +869,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $constraintName = $foreignKey->getName() ?: $tableName . '_' . implode('_', $foreignKey->getColumns()); + $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); $columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns())); $refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns())); @@ -881,6 +886,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta return $def; } + /** + * Generate a unique foreign key constraint name. + * + * @param string $tableName Table name + * @param array $columns Column names + * @return string + */ + protected function getUniqueForeignKeyName(string $tableName, array $columns): string + { + $baseName = $tableName . '_' . implode('_', $columns); + $maxLength = static::IDENTIFIER_MAX_LENGTH - 3; + if (strlen($baseName) > $maxLength) { + $baseName = substr($baseName, 0, $maxLength); + } + $existingKeys = $this->getForeignKeys($tableName); + $existingNames = array_column($existingKeys, 'name'); + + if (!in_array($baseName, $existingNames, true)) { + return $baseName; + } + + $counter = 2; + while (in_array($baseName . '_' . $counter, $existingNames, true)) { + $counter++; + } + + return $baseName . '_' . $counter; + } + /** * Creates the specified schema. * diff --git a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock index 10092ee0..9d56436c 100644 Binary files a/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock and b/tests/comparisons/Diff/default/schema-dump-test_comparisons_mysql.lock differ diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index 4dce76e4..679efe87 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -16,7 +16,7 @@ class TheDiffDefaultMysql extends BaseMigration public function up(): void { $this->table('articles') - ->dropForeignKey([], 'articles_ibfk_1') + ->dropForeignKey([], 'articles_user_id') ->removeIndexByName('UNIQUE_SLUG') ->removeIndexByName('rating_index') ->removeIndexByName('BY_NAME') @@ -86,7 +86,7 @@ public function up(): void ]) ->addIndex( $this->index('user_id') - ->setName('categories_ibfk_1') + ->setName('categories_user_id') ) ->addIndex( $this->index('name') @@ -101,7 +101,7 @@ public function up(): void ->setReferencedColumns('id') ->setDelete('RESTRICT') ->setUpdate('RESTRICT') - ->setName('categories_ibfk_1') + ->setName('categories_user_id') ) ->update(); @@ -234,7 +234,7 @@ public function down(): void ->setReferencedColumns('id') ->setDelete('CASCADE') ->setUpdate('CASCADE') - ->setName('articles_ibfk_1') + ->setName('articles_user_id') ) ->update(); diff --git a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php index 65565a19..a8f69ca9 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php @@ -72,7 +72,7 @@ public function up(): void ->setReferencedColumns('id') ->setDelete('RESTRICT') ->setUpdate('RESTRICT') - ->setName('articles_ibfk_1') + ->setName('articles_user_id') ) ->update(); }