Skip to content

Commit 347b4ab

Browse files
committed
Merge remote-tracking branch 'origin/5.x' into 5.next
# Conflicts: # src/Db/Adapter/MysqlAdapter.php # src/Db/Adapter/SqliteAdapter.php
2 parents d752038 + 4e108a3 commit 347b4ab

43 files changed

Lines changed: 396 additions & 182 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ error.log
1616
/tests/test_app/config/TestsMigrations/schema-dump-test.lock
1717
/tests/test_app/Plugin/TestBlog/config/Migrations/*
1818
.phpunit.cache
19+
.phpcs.cache
1920
.ddev
2021

2122
# IDE and editor specific files #

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
"@stan",
6969
"@test"
7070
],
71-
"cs-check": "phpcs --parallel=16 -p",
72-
"cs-fix": "phpcbf --parallel=16 -p",
71+
"cs-check": "phpcs",
72+
"cs-fix": "phpcbf",
7373
"phpstan": "tools/phpstan analyse",
7474
"stan": "@phpstan",
7575
"stan-baseline": "tools/phpstan --generate-baseline",

docs/en/upgrading.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,40 @@ insertOrSkip() for Seeds
130130
New ``insertOrSkip()`` method for seeds to insert records only if they don't already exist,
131131
making seeds more idempotent.
132132

133+
Foreign Key Constraint Naming
134+
=============================
135+
136+
Starting in 5.x, when you use ``addForeignKey()`` without providing an explicit constraint
137+
name, migrations will auto-generate a name using the pattern ``{table}_{columns}``.
138+
139+
Previously, MySQL would auto-generate constraint names (like ``articles_ibfk_1``), while
140+
PostgreSQL and SQL Server used migrations-generated names. Now all adapters use the same
141+
consistent naming pattern.
142+
143+
**Impact on existing migrations:**
144+
145+
If you have existing migrations that use ``addForeignKey()`` without explicit names, and
146+
later migrations that reference those constraints by name (e.g., in ``dropForeignKey()``),
147+
the generated names may differ between old and new migrations. This could cause
148+
``dropForeignKey()`` to fail if it's looking for a name that doesn't exist.
149+
150+
**Recommendations:**
151+
152+
1. For new migrations, you can rely on auto-generated names or provide explicit names
153+
2. If you have rollback issues with existing migrations, you may need to update them
154+
to use explicit constraint names
155+
3. The auto-generated names include conflict resolution - if ``{table}_{columns}`` already
156+
exists, a counter suffix is added (``_2``, ``_3``, etc.)
157+
158+
**Name length limits:**
159+
160+
Auto-generated names are truncated to respect database limits:
161+
162+
- MySQL: 61 characters (64 - 3 for counter suffix)
163+
- PostgreSQL: 60 characters (63 - 3)
164+
- SQL Server: 125 characters (128 - 3)
165+
- SQLite: No limit
166+
133167
Migration File Compatibility
134168
============================
135169

phpcs.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?xml version="1.0"?>
22
<ruleset name="CakePHP Core">
3-
<arg value="ns"/>
3+
<arg value="nps"/>
4+
<arg name="colors"/>
5+
<arg name="parallel" value="4"/>
6+
<arg name="cache" value=".phpcs.cache"/>
47

58
<file>src/</file>
69
<file>tests/</file>

src/Db/Adapter/MysqlAdapter.php

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
*/
3232
class MysqlAdapter extends AbstractAdapter
3333
{
34+
/**
35+
* Maximum length for identifiers (table names, column names, constraint names, etc.)
36+
*/
37+
protected const IDENTIFIER_MAX_LENGTH = 64;
38+
3439
/**
3540
* @var string[]
3641
*/
@@ -371,8 +376,10 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
371376
protected function mapColumnData(array $data): array
372377
{
373378
if ($data['type'] == self::TYPE_TEXT && $data['length'] !== null) {
379+
// Accept both migrations TEXT_LONG and CakePHP LENGTH_LONG for backward compatibility
380+
// with migrations generated before the fix (LENGTH_TINY/MEDIUM are already equal to TEXT_TINY/MEDIUM)
374381
$data['length'] = match ($data['length']) {
375-
self::TEXT_LONG => TableSchema::LENGTH_LONG,
382+
self::TEXT_LONG, TableSchema::LENGTH_LONG => TableSchema::LENGTH_LONG,
376383
self::TEXT_MEDIUM => TableSchema::LENGTH_MEDIUM,
377384
self::TEXT_REGULAR => null,
378385
self::TEXT_TINY => TableSchema::LENGTH_TINY,
@@ -979,7 +986,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey
979986
{
980987
$alter = sprintf(
981988
'ADD %s',
982-
$this->getForeignKeySqlDefinition($foreignKey),
989+
$this->getForeignKeySqlDefinition($foreignKey, $table->getName()),
983990
);
984991

985992
return new AlterInstructions([$alter]);
@@ -1194,15 +1201,13 @@ protected function getIndexSqlDefinition(Index $index): string
11941201
* Gets the MySQL Foreign Key Definition for an ForeignKey object.
11951202
*
11961203
* @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key
1204+
* @param string $tableName Table name for auto-generating constraint name
11971205
* @return string
11981206
*/
1199-
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
1207+
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
12001208
{
1201-
$def = '';
1202-
$name = $foreignKey->getName();
1203-
if ($name) {
1204-
$def .= ' CONSTRAINT ' . $this->quoteColumnName($name);
1205-
}
1209+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
1210+
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
12061211
$columnNames = [];
12071212
foreach ($foreignKey->getColumns() as $column) {
12081213
$columnNames[] = $this->quoteColumnName($column);
@@ -1229,6 +1234,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
12291234
return $def;
12301235
}
12311236

1237+
/**
1238+
* Generate a unique foreign key constraint name.
1239+
*
1240+
* @param string $tableName Table name
1241+
* @param array<string> $columns Column names
1242+
* @return string
1243+
*/
1244+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
1245+
{
1246+
$baseName = $tableName . '_' . implode('_', $columns);
1247+
$maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
1248+
if (strlen($baseName) > $maxLength) {
1249+
$baseName = substr($baseName, 0, $maxLength);
1250+
}
1251+
$existingKeys = $this->getForeignKeys($tableName);
1252+
$existingNames = array_column($existingKeys, 'name');
1253+
1254+
if (!in_array($baseName, $existingNames, true)) {
1255+
return $baseName;
1256+
}
1257+
1258+
$counter = 2;
1259+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
1260+
$counter++;
1261+
}
1262+
1263+
return $baseName . '_' . $counter;
1264+
}
1265+
12321266
/**
12331267
* Returns MySQL column types (inherited and MySQL specified).
12341268
*

src/Db/Adapter/PostgresAdapter.php

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

3030
class PostgresAdapter extends AbstractAdapter
3131
{
32+
/**
33+
* Maximum length for identifiers (table names, column names, constraint names, etc.)
34+
*/
35+
protected const IDENTIFIER_MAX_LENGTH = 63;
36+
3237
public const GENERATED_ALWAYS = 'ALWAYS';
3338
public const GENERATED_BY_DEFAULT = 'BY DEFAULT';
3439
/**
@@ -955,11 +960,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
955960
*/
956961
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
957962
{
958-
$parts = $this->getSchemaName($tableName);
959-
960-
$constraintName = $foreignKey->getName() ?: (
961-
$parts['table'] . '_' . implode('_', $foreignKey->getColumns()) . '_fkey'
962-
);
963+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
963964
$columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns()));
964965
$refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns()));
965966
$referencedTable = $foreignKey->getReferencedTable();
@@ -982,6 +983,36 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta
982983
return $def;
983984
}
984985

986+
/**
987+
* Generate a unique foreign key constraint name.
988+
*
989+
* @param string $tableName Table name
990+
* @param array<string> $columns Column names
991+
* @return string
992+
*/
993+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
994+
{
995+
$parts = $this->getSchemaName($tableName);
996+
$baseName = $parts['table'] . '_' . implode('_', $columns) . '_fkey';
997+
$maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
998+
if (strlen($baseName) > $maxLength) {
999+
$baseName = substr($baseName, 0, $maxLength);
1000+
}
1001+
$existingKeys = $this->getForeignKeys($tableName);
1002+
$existingNames = array_column($existingKeys, 'name');
1003+
1004+
if (!in_array($baseName, $existingNames, true)) {
1005+
return $baseName;
1006+
}
1007+
1008+
$counter = 2;
1009+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
1010+
$counter++;
1011+
}
1012+
1013+
return $baseName . '_' . $counter;
1014+
}
1015+
9851016
/**
9861017
* @inheritDoc
9871018
*/

src/Db/Adapter/SqliteAdapter.php

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

14331433
//Delete indexes from original table and recreate them in temporary table
14341434
$schema = $this->getSchemaName($tableName, true)['schema'];
@@ -1673,15 +1673,13 @@ public function getColumnTypes(): array
16731673
* Gets the SQLite Foreign Key Definition for an ForeignKey object.
16741674
*
16751675
* @param \Migrations\Db\Table\ForeignKey $foreignKey Foreign key
1676+
* @param string $tableName Table name for auto-generating constraint name
16761677
* @return string
16771678
*/
1678-
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
1679+
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
16791680
{
1680-
$def = '';
1681-
$name = $foreignKey->getName();
1682-
if ($name) {
1683-
$def .= ' CONSTRAINT ' . $this->quoteColumnName($name);
1684-
}
1681+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
1682+
$def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName);
16851683
$columnNames = [];
16861684
foreach ($foreignKey->getColumns() as $column) {
16871685
$columnNames[] = $this->quoteColumnName($column);
@@ -1706,6 +1704,31 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string
17061704
return $def;
17071705
}
17081706

1707+
/**
1708+
* Generate a unique foreign key constraint name.
1709+
*
1710+
* @param string $tableName Table name
1711+
* @param array<string> $columns Column names
1712+
* @return string
1713+
*/
1714+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
1715+
{
1716+
$baseName = $tableName . '_' . implode('_', $columns);
1717+
$existingKeys = $this->getForeignKeys($tableName);
1718+
$existingNames = array_column($existingKeys, 'name');
1719+
1720+
if (!in_array($baseName, $existingNames, true)) {
1721+
return $baseName;
1722+
}
1723+
1724+
$counter = 2;
1725+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
1726+
$counter++;
1727+
}
1728+
1729+
return $baseName . '_' . $counter;
1730+
}
1731+
17091732
/**
17101733
* @inheritDoc
17111734
*/

src/Db/Adapter/SqlserverAdapter.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
*/
3131
class SqlserverAdapter extends AbstractAdapter
3232
{
33+
/**
34+
* Maximum length for identifiers (table names, column names, constraint names, etc.)
35+
*/
36+
protected const IDENTIFIER_MAX_LENGTH = 128;
37+
3338
/**
3439
* @var string[]
3540
*/
@@ -874,7 +879,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin
874879
*/
875880
protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $tableName): string
876881
{
877-
$constraintName = $foreignKey->getName() ?: $tableName . '_' . implode('_', $foreignKey->getColumns());
882+
$constraintName = $foreignKey->getName() ?: $this->getUniqueForeignKeyName($tableName, $foreignKey->getColumns());
878883
$columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns()));
879884
$refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns()));
880885

@@ -895,6 +900,35 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta
895900
return $def;
896901
}
897902

903+
/**
904+
* Generate a unique foreign key constraint name.
905+
*
906+
* @param string $tableName Table name
907+
* @param array<string> $columns Column names
908+
* @return string
909+
*/
910+
protected function getUniqueForeignKeyName(string $tableName, array $columns): string
911+
{
912+
$baseName = $tableName . '_' . implode('_', $columns);
913+
$maxLength = static::IDENTIFIER_MAX_LENGTH - 3;
914+
if (strlen($baseName) > $maxLength) {
915+
$baseName = substr($baseName, 0, $maxLength);
916+
}
917+
$existingKeys = $this->getForeignKeys($tableName);
918+
$existingNames = array_column($existingKeys, 'name');
919+
920+
if (!in_array($baseName, $existingNames, true)) {
921+
return $baseName;
922+
}
923+
924+
$counter = 2;
925+
while (in_array($baseName . '_' . $counter, $existingNames, true)) {
926+
$counter++;
927+
}
928+
929+
return $baseName . '_' . $counter;
930+
}
931+
898932
/**
899933
* Creates the specified schema.
900934
*

src/Db/Table/ForeignKey.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ public function setOptions(array $options)
113113
throw new RuntimeException(sprintf('"%s" is not a valid foreign key option.', $option));
114114
}
115115

116-
// handle $options['delete'] as $options['update']
116+
// handle $options['delete'] and $options['update']
117117
if ($option === 'delete') {
118-
$this->setOnDelete($value);
118+
$this->delete = $this->normalizeAction($value);
119119
} elseif ($option === 'update') {
120-
$this->setOnUpdate($value);
120+
$this->update = $this->normalizeAction($value);
121121
} elseif ($option === 'deferrable') {
122122
$this->setDeferrableMode($value);
123123
} else {

src/View/Helper/MigrationHelper.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
use Cake\Database\Connection;
1919
use Cake\Database\Driver\Mysql;
2020
use Cake\Database\Schema\CollectionInterface;
21+
use Cake\Database\Schema\TableSchema;
2122
use Cake\Database\Schema\TableSchemaInterface;
2223
use Cake\Utility\Hash;
2324
use Cake\Utility\Inflector;
2425
use Cake\View\Helper;
2526
use Cake\View\View;
27+
use Migrations\Db\Adapter\MysqlAdapter;
2628
use Migrations\Db\Table\ForeignKey;
2729

2830
/**
@@ -401,6 +403,10 @@ public function getColumnOption(array $options): array
401403
if (empty($columnOptions['collate'])) {
402404
unset($columnOptions['collate']);
403405
}
406+
// isset() returns false for null values, so this handles both missing and null cases
407+
if (!isset($columnOptions['fixed'])) {
408+
unset($columnOptions['fixed']);
409+
}
404410

405411
// currently only MySQL supports the signed option
406412
$driver = $connection->getDriver();
@@ -441,6 +447,13 @@ public function getColumnOption(array $options): array
441447
}
442448
}
443449

450+
// Convert CakePHP's LENGTH_LONG to migrations TEXT_LONG for text columns
451+
// CakePHP uses LENGTH_LONG = 4294967295, but migrations expects TEXT_LONG = 2147483647
452+
// (LENGTH_TINY and LENGTH_MEDIUM have the same values as TEXT_TINY and TEXT_MEDIUM)
453+
if (isset($columnOptions['limit']) && $columnOptions['limit'] === TableSchema::LENGTH_LONG) {
454+
$columnOptions['limit'] = MysqlAdapter::TEXT_LONG;
455+
}
456+
444457
return $columnOptions;
445458
}
446459

0 commit comments

Comments
 (0)