From d557dd0dc76508ddb1f56e30091f402f7de63950 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 9 Mar 2026 01:12:34 +0100 Subject: [PATCH 1/5] 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. --- src/Db/Adapter/MysqlAdapter.php | 27 +++++++++++++++++++++++++- src/Db/Adapter/PostgresAdapter.php | 30 ++++++++++++++++++++++++++--- src/Db/Adapter/SqliteAdapter.php | 27 +++++++++++++++++++++++++- src/Db/Adapter/SqlserverAdapter.php | 27 +++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 01dfe8c1..662d8dfb 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -1197,7 +1197,7 @@ protected function getIndexSqlDefinition(Index $index): string */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $constraintName = $foreignKey->getName() ?: ($tableName . '_' . implode('_', $foreignKey->getColumns())); + $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $columnNames = []; foreach ($foreignKey->getColumns() as $column) { @@ -1221,6 +1221,31 @@ 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); + $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..3b678a84 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -951,9 +951,7 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta { $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 +970,32 @@ 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'; + $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 f78ff76b..82a3812d 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -1675,7 +1675,7 @@ public function getColumnTypes(): array */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $constraintName = $foreignKey->getName() ?: ($tableName . '_' . implode('_', $foreignKey->getColumns())); + $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $columnNames = []; foreach ($foreignKey->getColumns() as $column) { @@ -1697,6 +1697,31 @@ 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); + $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..f1dbad0c 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -864,7 +864,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 +881,31 @@ 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); + $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. * From e935d420f13f1cb9232148846b46b7af8181b356 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 9 Mar 2026 01:16:31 +0100 Subject: [PATCH 2/5] Remove unused variable --- src/Db/Adapter/PostgresAdapter.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 3b678a84..6ffd4774 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -949,8 +949,6 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin */ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string { - $parts = $this->getSchemaName($tableName); - $constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns()); $columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns())); $refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns())); From 948530945d765e9c68b3a362474af8f16a6e0b57 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 9 Mar 2026 16:03:16 +0100 Subject: [PATCH 3/5] 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). --- src/Db/Adapter/MysqlAdapter.php | 3 +++ src/Db/Adapter/PostgresAdapter.php | 3 +++ src/Db/Adapter/SqliteAdapter.php | 3 +++ src/Db/Adapter/SqlserverAdapter.php | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 662d8dfb..b2821e53 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -1231,6 +1231,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); + if (strlen($baseName) > 125) { + $baseName = substr($baseName, 0, 125); + } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 6ffd4774..ecc1a049 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -979,6 +979,9 @@ protected function getUniqueForeignKeyName(string $tableName, array $columns): s { $parts = $this->getSchemaName($tableName); $baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey'; + if (strlen($baseName) > 125) { + $baseName = substr($baseName, 0, 125); + } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 82a3812d..a3c0c319 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -1707,6 +1707,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); + if (strlen($baseName) > 125) { + $baseName = substr($baseName, 0, 125); + } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index f1dbad0c..012dc259 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -891,6 +891,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); + if (strlen($baseName) > 125) { + $baseName = substr($baseName, 0, 125); + } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); From 65ee29757254028fe2658aa475742dee0a827da2 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 9 Mar 2026 16:05:25 +0100 Subject: [PATCH 4/5] 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 --- src/Db/Adapter/MysqlAdapter.php | 4 ++-- src/Db/Adapter/PostgresAdapter.php | 4 ++-- src/Db/Adapter/SqliteAdapter.php | 3 --- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index b2821e53..fc212702 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -1231,8 +1231,8 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); - if (strlen($baseName) > 125) { - $baseName = substr($baseName, 0, 125); + if (strlen($baseName) > 61) { + $baseName = substr($baseName, 0, 61); } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index ecc1a049..ee52c2cb 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -979,8 +979,8 @@ protected function getUniqueForeignKeyName(string $tableName, array $columns): s { $parts = $this->getSchemaName($tableName); $baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey'; - if (strlen($baseName) > 125) { - $baseName = substr($baseName, 0, 125); + if (strlen($baseName) > 60) { + $baseName = substr($baseName, 0, 60); } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index a3c0c319..82a3812d 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -1707,9 +1707,6 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); - if (strlen($baseName) > 125) { - $baseName = substr($baseName, 0, 125); - } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); From 9089e17a59c2e1f8d98d30c3d6fb44909445521d Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 9 Mar 2026 16:07:33 +0100 Subject: [PATCH 5/5] 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. --- src/Db/Adapter/MysqlAdapter.php | 10 ++++++++-- src/Db/Adapter/PostgresAdapter.php | 10 ++++++++-- src/Db/Adapter/SqlserverAdapter.php | 10 ++++++++-- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index fc212702..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[] */ @@ -1231,8 +1236,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); - if (strlen($baseName) > 61) { - $baseName = substr($baseName, 0, 61); + $maxLength = static::IDENTIFIER_MAX_LENGTH - 3; + if (strlen($baseName) > $maxLength) { + $baseName = substr($baseName, 0, $maxLength); } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index ee52c2cb..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'; /** @@ -979,8 +984,9 @@ protected function getUniqueForeignKeyName(string $tableName, array $columns): s { $parts = $this->getSchemaName($tableName); $baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey'; - if (strlen($baseName) > 60) { - $baseName = substr($baseName, 0, 60); + $maxLength = static::IDENTIFIER_MAX_LENGTH - 3; + if (strlen($baseName) > $maxLength) { + $baseName = substr($baseName, 0, $maxLength); } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name'); diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index 012dc259..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[] */ @@ -891,8 +896,9 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta protected function getUniqueForeignKeyName(string $tableName, array $columns): string { $baseName = $tableName . '_' . implode('_', $columns); - if (strlen($baseName) > 125) { - $baseName = substr($baseName, 0, 125); + $maxLength = static::IDENTIFIER_MAX_LENGTH - 3; + if (strlen($baseName) > $maxLength) { + $baseName = substr($baseName, 0, $maxLength); } $existingKeys = $this->getForeignKeys($tableName); $existingNames = array_column($existingKeys, 'name');