From 4085be7f02c50042a036cb408dc484d6ae3c89cc Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 8 Mar 2026 22:00:57 +0100 Subject: [PATCH 01/12] 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. --- src/Db/Adapter/MysqlAdapter.php | 11 +++++------ src/Db/Adapter/SqliteAdapter.php | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index a8aa1e8c..01dfe8c1 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -977,7 +977,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 +1192,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() ?: ($tableName . '_' . implode('_', $foreignKey->getColumns())); + $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $columnNames = []; foreach ($foreignKey->getColumns() as $column) { $columnNames[] = $this->quoteColumnName($column); diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 9145e0cb..f78ff76b 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() ?: ($tableName . '_' . implode('_', $foreignKey->getColumns())); + $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $columnNames = []; foreach ($foreignKey->getColumns() as $column) { $columnNames[] = $this->quoteColumnName($column); From 629c85e49aee95c9206fac2df58c1cb182dbbf5a Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 8 Mar 2026 22:01:50 +0100 Subject: [PATCH 02/12] 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). --- .../schema-dump-test_comparisons_mysql.lock | Bin 8605 -> 8607 bytes .../Diff/default/the_diff_default_mysql.php | 8 ++++---- .../Diff/simple/the_diff_simple_mysql.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) 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 10092ee02d595b356d45800afedae1bd4fa91e8b..340a2713d99cb3a2a61d65a07a2680a95329fec4 100644 GIT binary patch delta 57 zcmbR1Jl}akxsb4#l~Q6+NoI0RYH@sNacWU~X3FGy60(z*2`TU(%S|p6aNaB;+{p(3 DWl|Jx delta 62 zcmbR5JlAtable('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') ->setOnDelete('RESTRICT') ->setOnUpdate('RESTRICT') - ->setName('categories_ibfk_1') + ->setName('categories_user_id') ) ->update(); @@ -234,7 +234,7 @@ public function down(): void ->setReferencedColumns('id') ->setOnDelete('CASCADE') ->setOnUpdate('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 93edeb6d..a9224032 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') ->setOnDelete('RESTRICT') ->setOnUpdate('RESTRICT') - ->setName('articles_ibfk_1') + ->setName('articles_user_id') ) ->update(); } From 22eb54e695d05c5e6e1244bf52d3ce997d254430 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 8 Mar 2026 22:08:08 +0100 Subject: [PATCH 03/12] 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. --- .../schema-dump-test_comparisons_mysql.lock | Bin 8607 -> 8627 bytes .../Diff/default/the_diff_default_mysql.php | 10 ++++++++++ 2 files changed, 10 insertions(+) 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 340a2713d99cb3a2a61d65a07a2680a95329fec4..9d56436ca2229477ac0d0b9eb70a113e273b0b84 100644 GIT binary patch delta 38 ucmbR5yxDn!t^lv0nUzvvQAuWUPHJ)dWCH=a$;X5OCV%G(-^?vIMF0Q>LJfKV delta 28 kcmdn&Jl}bPt^l*SmC|G@0lUdEJbaTs30Z7DA@EuN0EK1=l>h($ diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index 5c6e8dd7..bd3532e1 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -20,6 +20,7 @@ public function up(): void ->removeIndexByName('UNIQUE_SLUG') ->removeIndexByName('rating_index') ->removeIndexByName('BY_NAME') + ->removeIndexByName('user_id') ->update(); $this->table('articles') @@ -124,6 +125,10 @@ public function up(): void $this->index('slug') ->setName('UNIQUE_SLUG') ) + ->addIndex( + $this->index('user_id') + ->setName('articles_user_id') + ) ->addIndex( $this->index('category_id') ->setName('category_id') @@ -152,6 +157,7 @@ public function down(): void $this->table('articles') ->removeIndexByName('UNIQUE_SLUG') + ->removeIndexByName('articles_user_id') ->removeIndexByName('category_id') ->removeIndexByName('rating_index') ->update(); @@ -207,6 +213,10 @@ public function down(): void $this->index('name') ->setName('BY_NAME') ) + ->addIndex( + $this->index('user_id') + ->setName('user_id') + ) ->update(); $this->table('tags') From 2897bf25a8ebe7c7d5d77349cb04bb6aa59cd1ef Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 8 Mar 2026 22:14:41 +0100 Subject: [PATCH 04/12] 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. --- .../Diff/default/the_diff_default_mysql.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index bd3532e1..5c6e8dd7 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -20,7 +20,6 @@ public function up(): void ->removeIndexByName('UNIQUE_SLUG') ->removeIndexByName('rating_index') ->removeIndexByName('BY_NAME') - ->removeIndexByName('user_id') ->update(); $this->table('articles') @@ -125,10 +124,6 @@ public function up(): void $this->index('slug') ->setName('UNIQUE_SLUG') ) - ->addIndex( - $this->index('user_id') - ->setName('articles_user_id') - ) ->addIndex( $this->index('category_id') ->setName('category_id') @@ -157,7 +152,6 @@ public function down(): void $this->table('articles') ->removeIndexByName('UNIQUE_SLUG') - ->removeIndexByName('articles_user_id') ->removeIndexByName('category_id') ->removeIndexByName('rating_index') ->update(); @@ -213,10 +207,6 @@ public function down(): void $this->index('name') ->setName('BY_NAME') ) - ->addIndex( - $this->index('user_id') - ->setName('user_id') - ) ->update(); $this->table('tags') From fc56383be59fd7a73631f9f127abb663fb8a5b62 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 11 Mar 2026 18:53:35 +0100 Subject: [PATCH 05/12] 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. --- src/Db/Adapter/MysqlAdapter.php | 36 ++++++++++++++++++++++++- src/Db/Adapter/PostgresAdapter.php | 41 +++++++++++++++++++++++++---- src/Db/Adapter/SqliteAdapter.php | 27 ++++++++++++++++++- src/Db/Adapter/SqlserverAdapter.php | 36 ++++++++++++++++++++++++- 4 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 01dfe8c1..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[] */ @@ -1197,7 +1202,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 +1226,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; + } + /** * 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 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..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. * From 6d4b00d706e715b4f07b9f00030f860804dcda37 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 11 Mar 2026 20:47:31 +0100 Subject: [PATCH 06/12] Filter out null fixed option from generated migrations (#1048) When generating migrations, the 'fixed' option could be set to null for binary columns, which causes an error when running the migration because Column::setFixed() expects a bool, not null. Fixes #1046 --- src/View/Helper/MigrationHelper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 75e04cbb..b0014f7c 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -401,6 +401,10 @@ public function getColumnOption(array $options): array if (empty($columnOptions['collate'])) { unset($columnOptions['collate']); } + // isset() returns false for null values, so this handles both missing and null cases + if (!isset($columnOptions['fixed'])) { + unset($columnOptions['fixed']); + } // currently only MySQL supports the signed option $driver = $connection->getDriver(); From ea004d1b6ce448cc49914e5d858271a7a1146223 Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 11 Mar 2026 20:54:16 +0100 Subject: [PATCH 07/12] Document foreign key constraint naming changes Add upgrade documentation explaining the new auto-generated FK constraint naming behavior introduced in #1041 and #1042, including: - New consistent naming pattern across all adapters - Potential impact on existing migrations with rollbacks - Conflict resolution with counter suffixes - Database-specific name length limits --- docs/en/upgrading.rst | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/en/upgrading.rst b/docs/en/upgrading.rst index 23ab2e11..d413bff4 100644 --- a/docs/en/upgrading.rst +++ b/docs/en/upgrading.rst @@ -130,6 +130,40 @@ insertOrSkip() for Seeds New ``insertOrSkip()`` method for seeds to insert records only if they don't already exist, making seeds more idempotent. +Foreign Key Constraint Naming +============================= + +Starting in 5.x, when you use ``addForeignKey()`` without providing an explicit constraint +name, migrations will auto-generate a name using the pattern ``{table}_{columns}``. + +Previously, MySQL would auto-generate constraint names (like ``articles_ibfk_1``), while +PostgreSQL and SQL Server used migrations-generated names. Now all adapters use the same +consistent naming pattern. + +**Impact on existing migrations:** + +If you have existing migrations that use ``addForeignKey()`` without explicit names, and +later migrations that reference those constraints by name (e.g., in ``dropForeignKey()``), +the generated names may differ between old and new migrations. This could cause +``dropForeignKey()`` to fail if it's looking for a name that doesn't exist. + +**Recommendations:** + +1. For new migrations, you can rely on auto-generated names or provide explicit names +2. If you have rollback issues with existing migrations, you may need to update them + to use explicit constraint names +3. The auto-generated names include conflict resolution - if ``{table}_{columns}`` already + exists, a counter suffix is added (``_2``, ``_3``, etc.) + +**Name length limits:** + +Auto-generated names are truncated to respect database limits: + +- MySQL: 61 characters (64 - 3 for counter suffix) +- PostgreSQL: 60 characters (63 - 3) +- SQL Server: 125 characters (128 - 3) +- SQLite: No limit + Migration File Compatibility ============================ From 273b420bef66e716d04d734927040713acc8d72c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 11 Mar 2026 21:13:36 +0100 Subject: [PATCH 08/12] Stop using deprecated setOnDelete/setOnUpdate methods (#1047) Update bake templates and internal code to use the non-deprecated setDelete() and setUpdate() methods instead of the deprecated setOnDelete() and setOnUpdate() methods. Fixes #1045 --- src/Db/Table/ForeignKey.php | 6 +++--- templates/bake/element/add-foreign-keys.twig | 4 ++-- templates/bake/element/change-method-body.twig | 4 ++-- tests/TestCase/Db/Table/TableTest.php | 3 +-- .../Diff/default/the_diff_default_mysql.php | 8 ++++---- .../Diff/default/the_diff_default_pgsql.php | 12 ++++++------ .../Diff/simple/the_diff_simple_mysql.php | 4 ++-- .../pgsql/test_snapshot_auto_id_disabled_pgsql.php | 12 ++++++------ .../pgsql/test_snapshot_not_empty_pgsql.php | 12 ++++++------ .../pgsql/test_snapshot_plugin_blog_pgsql.php | 12 ++++++------ .../pgsql/test_snapshot_with_change_pgsql.php | 12 ++++++------ .../sqlite/test_snapshot_auto_id_disabled_sqlite.php | 12 ++++++------ .../sqlite/test_snapshot_not_empty_sqlite.php | 12 ++++++------ .../sqlite/test_snapshot_plugin_blog_sqlite.php | 12 ++++++------ .../sqlite/test_snapshot_with_change_sqlite.php | 12 ++++++------ .../test_snapshot_auto_id_disabled_sqlserver.php | 12 ++++++------ .../sqlserver/test_snapshot_not_empty_sqlserver.php | 12 ++++++------ .../test_snapshot_plugin_blog_sqlserver.php | 12 ++++++------ .../test_snapshot_with_change_sqlserver.php | 12 ++++++------ .../Migration/testAddFieldWithReference.php | 4 ++-- .../Migration/testCreateWithReferences.php | 4 ++-- .../testCreateWithReferencesCustomTable.php | 4 ++-- .../Migration/test_snapshot_auto_id_disabled.php | 12 ++++++------ .../Migration/test_snapshot_not_empty.php | 12 ++++++------ .../Migration/test_snapshot_plugin_blog.php | 12 ++++++------ .../test_snapshot_postgres_timestamp_tz_pgsql.php | 12 ++++++------ ...t_with_auto_id_compatible_signed_primary_keys.php | 12 ++++++------ ...with_auto_id_incompatible_signed_primary_keys.php | 12 ++++++------ ...th_auto_id_incompatible_unsigned_primary_keys.php | 12 ++++++------ .../Migration/test_snapshot_with_change.php | 12 ++++++------ .../test_snapshot_with_non_default_collation.php | 12 ++++++------ 31 files changed, 152 insertions(+), 153 deletions(-) diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php index 907e5f37..3e4a4d39 100644 --- a/src/Db/Table/ForeignKey.php +++ b/src/Db/Table/ForeignKey.php @@ -100,11 +100,11 @@ public function setOptions(array $options) throw new RuntimeException(sprintf('"%s" is not a valid foreign key option.', $option)); } - // handle $options['delete'] as $options['update'] + // handle $options['delete'] and $options['update'] if ($option === 'delete') { - $this->setOnDelete($value); + $this->delete = $this->normalizeAction($value); } elseif ($option === 'update') { - $this->setOnUpdate($value); + $this->update = $this->normalizeAction($value); } elseif ($option === 'deferrable') { $this->setDeferrableMode($value); } else { diff --git a/templates/bake/element/add-foreign-keys.twig b/templates/bake/element/add-foreign-keys.twig index 774cf80d..9f67c0e5 100644 --- a/templates/bake/element/add-foreign-keys.twig +++ b/templates/bake/element/add-foreign-keys.twig @@ -28,8 +28,8 @@ $this->foreignKey({{ columnsList | raw }}) ->setReferencedTable('{{ constraint['references'][0] }}') ->setReferencedColumns({{ columnsReference | raw }}) - ->setOnDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}') - ->setOnUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}') + ->setDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}') + ->setUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}') ->setName('{{ constraintName }}') ) {%~ endif %} diff --git a/templates/bake/element/change-method-body.twig b/templates/bake/element/change-method-body.twig index e1370cd4..e36e7fb4 100644 --- a/templates/bake/element/change-method-body.twig +++ b/templates/bake/element/change-method-body.twig @@ -59,8 +59,8 @@ $this->foreignKey({{ columnsList | raw }}) ->setReferencedTable('{{ constraint['references'][0] }}') ->setReferencedColumns({{ columnsReference | raw }}) - ->setOnDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}') - ->setOnUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}') + ->setDelete('{{ Migration.formatConstraintAction(constraint['delete']) | raw }}') + ->setUpdate('{{ Migration.formatConstraintAction(constraint['update']) | raw }}') ->setName('{{ constraintName }}') ); {%~ endif %} diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 21912fd5..463e9aa8 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -144,8 +144,7 @@ public function testAddForeignKeyWithObject(): void $key->setColumns('user_id') ->setReferencedTable('users') ->setReferencedColumns(['id']) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setOptions(['delete' => 'CASCADE', 'update' => 'CASCADE']) ->setName('fk_user_id'), ); diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index a889d9e7..4dce76e4 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -99,8 +99,8 @@ public function up(): void $this->foreignKey('user_id') ->setReferencedTable('users') ->setReferencedColumns('id') - ->setOnDelete('RESTRICT') - ->setOnUpdate('RESTRICT') + ->setDelete('RESTRICT') + ->setUpdate('RESTRICT') ->setName('categories_ibfk_1') ) ->update(); @@ -232,8 +232,8 @@ public function down(): void $this->foreignKey('user_id') ->setReferencedTable('users') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('articles_ibfk_1') ) ->update(); diff --git a/tests/comparisons/Diff/default/the_diff_default_pgsql.php b/tests/comparisons/Diff/default/the_diff_default_pgsql.php index 54b61d85..52e1f076 100644 --- a/tests/comparisons/Diff/default/the_diff_default_pgsql.php +++ b/tests/comparisons/Diff/default/the_diff_default_pgsql.php @@ -64,8 +64,8 @@ public function up(): void $this->foreignKey('user_id') ->setReferencedTable('users') ->setReferencedColumns('id') - ->setOnDelete('RESTRICT') - ->setOnUpdate('RESTRICT') + ->setDelete('RESTRICT') + ->setUpdate('RESTRICT') ) ->update(); @@ -114,8 +114,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ) ->update(); @@ -207,8 +207,8 @@ public function down(): void $this->foreignKey('user_id') ->setReferencedTable('users') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ) ->update(); diff --git a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php index 93edeb6d..65565a19 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php @@ -70,8 +70,8 @@ public function up(): void $this->foreignKey('user_id') ->setReferencedTable('users') ->setReferencedColumns('id') - ->setOnDelete('RESTRICT') - ->setOnUpdate('RESTRICT') + ->setDelete('RESTRICT') + ->setUpdate('RESTRICT') ->setName('articles_ibfk_1') ) ->update(); diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php index eca100cc..bbe49420 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_auto_id_disabled_pgsql.php @@ -370,8 +370,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -387,8 +387,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -398,8 +398,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php index e297c18c..8ff44ed6 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php @@ -310,8 +310,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -327,8 +327,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -338,8 +338,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php index 2541ce03..25f27019 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php @@ -310,8 +310,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -327,8 +327,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -338,8 +338,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php b/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php index 1191bd8c..2c3cb4f5 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php @@ -310,8 +310,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -327,8 +327,8 @@ public function change(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -338,8 +338,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php index 24da5e35..12261cdd 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_auto_id_disabled_sqlite.php @@ -351,8 +351,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -368,8 +368,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -379,8 +379,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php index e292c18e..bef6d8ce 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php @@ -291,8 +291,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -308,8 +308,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -319,8 +319,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php index 78ae984e..f9f84028 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php @@ -291,8 +291,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -308,8 +308,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -319,8 +319,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php b/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php index f1fbd6ee..857bbde1 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php @@ -291,8 +291,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -308,8 +308,8 @@ public function change(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -319,8 +319,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php index 347b5cdd..47b630bf 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_auto_id_disabled_sqlserver.php @@ -386,8 +386,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -403,8 +403,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -414,8 +414,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php index e0aad74e..aa2bc9a3 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php @@ -326,8 +326,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -343,8 +343,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -354,8 +354,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php index c8d0e8db..8e49310d 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php @@ -326,8 +326,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -343,8 +343,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -354,8 +354,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php b/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php index 36ef3a9b..f1282faf 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php @@ -326,8 +326,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -343,8 +343,8 @@ public function change(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -354,8 +354,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/testAddFieldWithReference.php b/tests/comparisons/Migration/testAddFieldWithReference.php index b50ad2f9..b6c5ccfb 100644 --- a/tests/comparisons/Migration/testAddFieldWithReference.php +++ b/tests/comparisons/Migration/testAddFieldWithReference.php @@ -25,8 +25,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('fk_category_id') ); $table->update(); diff --git a/tests/comparisons/Migration/testCreateWithReferences.php b/tests/comparisons/Migration/testCreateWithReferences.php index 11c01ca2..5537e448 100644 --- a/tests/comparisons/Migration/testCreateWithReferences.php +++ b/tests/comparisons/Migration/testCreateWithReferences.php @@ -30,8 +30,8 @@ public function change(): void $this->foreignKey('user_id') ->setReferencedTable('users') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('fk_user_id') ); $table->create(); diff --git a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php index a4829f3c..6897dd2a 100644 --- a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php +++ b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php @@ -30,8 +30,8 @@ public function change(): void $this->foreignKey('author_id') ->setReferencedTable('authors') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('fk_author_id') ); $table->create(); diff --git a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php index 9024c29a..c8d07b21 100644 --- a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php +++ b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php @@ -376,8 +376,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -393,8 +393,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -404,8 +404,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_not_empty.php b/tests/comparisons/Migration/test_snapshot_not_empty.php index 1a277d71..4bde415d 100644 --- a/tests/comparisons/Migration/test_snapshot_not_empty.php +++ b/tests/comparisons/Migration/test_snapshot_not_empty.php @@ -308,8 +308,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -325,8 +325,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -336,8 +336,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_plugin_blog.php b/tests/comparisons/Migration/test_snapshot_plugin_blog.php index 20dd3fba..d3367359 100644 --- a/tests/comparisons/Migration/test_snapshot_plugin_blog.php +++ b/tests/comparisons/Migration/test_snapshot_plugin_blog.php @@ -308,8 +308,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -325,8 +325,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -336,8 +336,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php b/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php index 51b0002a..e4f7da4f 100644 --- a/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php +++ b/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php @@ -320,8 +320,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -337,8 +337,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -348,8 +348,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php index df2ea8ba..3da752b2 100644 --- a/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php +++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_compatible_signed_primary_keys.php @@ -375,8 +375,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -392,8 +392,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -403,8 +403,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php index 11fadb44..13f8327d 100644 --- a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php +++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_signed_primary_keys.php @@ -375,8 +375,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -392,8 +392,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -403,8 +403,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php index 93570e1e..bdf09994 100644 --- a/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php +++ b/tests/comparisons/Migration/test_snapshot_with_auto_id_incompatible_unsigned_primary_keys.php @@ -376,8 +376,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -393,8 +393,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -404,8 +404,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_with_change.php b/tests/comparisons/Migration/test_snapshot_with_change.php index ec5f12a6..d4a43233 100644 --- a/tests/comparisons/Migration/test_snapshot_with_change.php +++ b/tests/comparisons/Migration/test_snapshot_with_change.php @@ -308,8 +308,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -325,8 +325,8 @@ public function change(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -336,8 +336,8 @@ public function change(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); diff --git a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php index 45cad82a..c48d00a4 100644 --- a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php +++ b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php @@ -309,8 +309,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('NO_ACTION') - ->setOnUpdate('NO_ACTION') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') ->setName('articles_category_fk') ) ->update(); @@ -326,8 +326,8 @@ public function up(): void 'category_id', 'id', ]) - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('orders_product_fk') ) ->update(); @@ -337,8 +337,8 @@ public function up(): void $this->foreignKey('category_id') ->setReferencedTable('categories') ->setReferencedColumns('id') - ->setOnDelete('CASCADE') - ->setOnUpdate('CASCADE') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') ->setName('products_category_fk') ) ->update(); From c8cfb9359cec6a880f67eb25210fb02f260a5bd8 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 12 Mar 2026 22:10:11 +0100 Subject: [PATCH 09/12] Fix LONGTEXT columns becoming TEXT in generated migrations (#1050) The issue was a mismatch between CakePHP's LENGTH_LONG constant (4294967295) and migrations' TEXT_LONG constant (2147483647). When using `bake migration_diff`, CakePHP's schema reflection returns LENGTH_LONG for LONGTEXT columns, but MysqlAdapter expected TEXT_LONG. This fix: 1. MigrationHelper: Convert LENGTH_LONG to TEXT_LONG when generating migrations 2. MysqlAdapter: Accept both constants for backward compatibility with existing migrations that have the wrong value Fixes #1029 --- src/Db/Adapter/MysqlAdapter.php | 4 +- src/View/Helper/MigrationHelper.php | 9 +++++ .../TestCase/Db/Adapter/MysqlAdapterTest.php | 4 ++ .../View/Helper/MigrationHelperTest.php | 38 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 694f821b..dabf3a95 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -374,8 +374,10 @@ public function createTable(TableMetadata $table, array $columns = [], array $in protected function mapColumnData(array $data): array { if ($data['type'] == self::TYPE_TEXT && $data['length'] !== null) { + // Accept both migrations TEXT_LONG and CakePHP LENGTH_LONG for backward compatibility + // with migrations generated before the fix (LENGTH_TINY/MEDIUM are already equal to TEXT_TINY/MEDIUM) $data['length'] = match ($data['length']) { - self::TEXT_LONG => TableSchema::LENGTH_LONG, + self::TEXT_LONG, TableSchema::LENGTH_LONG => TableSchema::LENGTH_LONG, self::TEXT_MEDIUM => TableSchema::LENGTH_MEDIUM, self::TEXT_REGULAR => null, self::TEXT_TINY => TableSchema::LENGTH_TINY, diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index b0014f7c..9aec4dae 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -18,11 +18,13 @@ use Cake\Database\Connection; use Cake\Database\Driver\Mysql; use Cake\Database\Schema\CollectionInterface; +use Cake\Database\Schema\TableSchema; use Cake\Database\Schema\TableSchemaInterface; use Cake\Utility\Hash; use Cake\Utility\Inflector; use Cake\View\Helper; use Cake\View\View; +use Migrations\Db\Adapter\MysqlAdapter; use Migrations\Db\Table\ForeignKey; /** @@ -445,6 +447,13 @@ public function getColumnOption(array $options): array } } + // Convert CakePHP's LENGTH_LONG to migrations TEXT_LONG for text columns + // CakePHP uses LENGTH_LONG = 4294967295, but migrations expects TEXT_LONG = 2147483647 + // (LENGTH_TINY and LENGTH_MEDIUM have the same values as TEXT_TINY and TEXT_MEDIUM) + if (isset($columnOptions['limit']) && $columnOptions['limit'] === TableSchema::LENGTH_LONG) { + $columnOptions['limit'] = MysqlAdapter::TEXT_LONG; + } + return $columnOptions; } diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 1ab36b5c..94202d83 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -9,6 +9,7 @@ use Cake\Core\Configure; use Cake\Database\Connection; use Cake\Database\Driver\Mysql; +use Cake\Database\Schema\TableSchema; use Cake\Datasource\ConnectionManager; use InvalidArgumentException; use Migrations\Db\Adapter\MysqlAdapter; @@ -1377,6 +1378,9 @@ public static function textRoundTripData() ['text', MysqlAdapter::TEXT_TINY, 'text', MysqlAdapter::TEXT_TINY], ['text', MysqlAdapter::TEXT_MEDIUM, 'text', MysqlAdapter::TEXT_MEDIUM], ['text', MysqlAdapter::TEXT_LONG, 'text', MysqlAdapter::TEXT_LONG], + // Test backward compatibility: CakePHP's LENGTH_LONG (4294967295) should also work + // This ensures migrations generated before the fix still create LONGTEXT correctly + ['text', TableSchema::LENGTH_LONG, 'text', MysqlAdapter::TEXT_LONG], ]; } diff --git a/tests/TestCase/View/Helper/MigrationHelperTest.php b/tests/TestCase/View/Helper/MigrationHelperTest.php index fd2dfe91..0f64e4dd 100644 --- a/tests/TestCase/View/Helper/MigrationHelperTest.php +++ b/tests/TestCase/View/Helper/MigrationHelperTest.php @@ -16,9 +16,11 @@ use Cake\Database\Driver\Mysql; use Cake\Database\Driver\Sqlserver; use Cake\Database\Schema\Collection; +use Cake\Database\Schema\TableSchema; use Cake\Datasource\ConnectionManager; use Cake\TestSuite\TestCase; use Cake\View\View; +use Migrations\Db\Adapter\MysqlAdapter; use Migrations\View\Helper\MigrationHelper; use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; @@ -490,4 +492,40 @@ public function testGetColumnOptionExcludesFixedWhenNotSet(): void $this->assertArrayNotHasKey('fixed', $result); } + + /** + * Test that getColumnOption converts CakePHP's LENGTH_LONG to migrations TEXT_LONG + * + * CakePHP uses LENGTH_LONG = 4294967295 for LONGTEXT, but migrations expects + * TEXT_LONG = 2147483647. This ensures generated migrations use the correct value. + */ + public function testGetColumnOptionConvertsLengthLongToTextLong(): void + { + $options = [ + 'limit' => TableSchema::LENGTH_LONG, // 4294967295 + 'null' => true, + 'default' => null, + ]; + + $result = $this->helper->getColumnOption($options); + + $this->assertArrayHasKey('limit', $result); + $this->assertSame(MysqlAdapter::TEXT_LONG, $result['limit']); // 2147483647 + } + + /** + * Test that getColumnOption preserves other limit values unchanged + */ + public function testGetColumnOptionPreservesOtherLimits(): void + { + $options = [ + 'limit' => 255, // TEXT_TINY / LENGTH_TINY - same value + 'null' => true, + 'default' => null, + ]; + + $result = $this->helper->getColumnOption($options); + + $this->assertSame(255, $result['limit']); + } } From ad0e0a2a9dec3c6d0a2ac89a991d1dbdc9c09b7d Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 15 Mar 2026 02:05:22 +0100 Subject: [PATCH 10/12] Add support for PostgreSQL index access methods (gist, spgist, brin, hash) - Add Index constants for GIN, GIST, SPGIST, BRIN, and HASH access methods - Add opclass option for specifying operator classes on index columns - Generalize PostgresAdapter to support all access method types - Add tests for each new index type - Update documentation with examples for all PostgreSQL index types Refs cakephp/phinx#1823 --- docs/en/writing-migrations.rst | 102 +++++++++++- src/Db/Adapter/PostgresAdapter.php | 39 +++-- src/Db/Table/Index.php | 72 ++++++++- .../Db/Adapter/PostgresAdapterTest.php | 146 ++++++++++++++++++ 4 files changed, 345 insertions(+), 14 deletions(-) diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst index 6da4195d..e464e1b3 100644 --- a/docs/en/writing-migrations.rst +++ b/docs/en/writing-migrations.rst @@ -1284,7 +1284,15 @@ during index creation:: } } -PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes:: +PostgreSQL Index Access Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PostgreSQL supports several index access methods beyond the default B-tree. +Use the ``type`` option to specify the access method. + +**GIN (Generalized Inverted Index)** + +GIN indexes are useful for full-text search, arrays, and JSONB columns:: table('users'); - $table->addColumn('address', 'string') - ->addIndex('address', ['type' => 'gin']) + $table = $this->table('articles'); + $table->addColumn('tags', 'jsonb') + ->addIndex('tags', ['type' => 'gin']) + ->create(); + } + } + +**GiST (Generalized Search Tree)** + +GiST indexes support geometric data, range types, and full-text search. +For trigram similarity searches (requires the ``pg_trgm`` extension), use +the ``opclass`` option:: + + table('products'); + $table->addColumn('name', 'string') + ->addIndex('name', [ + 'type' => 'gist', + 'opclass' => ['name' => 'gist_trgm_ops'], + ]) + ->create(); + } + } + +**BRIN (Block Range Index)** + +BRIN indexes are highly efficient for large, naturally-ordered tables like +time-series data. They are much smaller than B-tree indexes but only work well +when data is physically ordered by the indexed column:: + + table('sensor_readings'); + $table->addColumn('recorded_at', 'timestamp') + ->addColumn('value', 'decimal') + ->addIndex('recorded_at', ['type' => 'brin']) + ->create(); + } + } + +**SP-GiST (Space-Partitioned GiST)** + +SP-GiST indexes work well for data with natural clustering, like IP addresses +or phone numbers:: + + table('access_logs'); + $table->addColumn('client_ip', 'inet') + ->addIndex('client_ip', ['type' => 'spgist']) + ->create(); + } + } + +**Hash** + +Hash indexes handle simple equality comparisons. They are rarely needed since +B-tree handles equality efficiently too:: + + table('sessions'); + $table->addColumn('session_id', 'string', ['limit' => 64]) + ->addIndex('session_id', ['type' => 'hash']) ->create(); } } diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index b0e61d22..2d6a9c23 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -54,7 +54,18 @@ class PostgresAdapter extends AbstractAdapter self::TYPE_NATIVE_UUID, ]; - private const GIN_INDEX_TYPE = 'gin'; + /** + * PostgreSQL index access methods that require USING clause. + * + * @var array + */ + private const ACCESS_METHOD_TYPES = [ + Index::GIN, + Index::GIST, + Index::SPGIST, + Index::BRIN, + Index::HASH, + ]; /** * Columns with comments @@ -910,8 +921,16 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin } $order = $index->getOrder() ?? []; - $columnNames = array_map(function ($columnName) use ($order) { + $opclass = $index->getOpclass() ?? []; + $columnNames = array_map(function ($columnName) use ($order, $opclass) { $ret = '"' . $columnName . '"'; + + // Add operator class if specified (e.g., gist_trgm_ops) + if (isset($opclass[$columnName])) { + $ret .= ' ' . $opclass[$columnName]; + } + + // Add ordering if specified (e.g., ASC NULLS FIRST) if (isset($order[$columnName])) { $ret .= ' ' . $order[$columnName]; } @@ -922,23 +941,25 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin $include = $index->getInclude(); $includedColumns = $include ? sprintf(' INCLUDE ("%s")', implode('","', $include)) : ''; - $createIndexSentence = 'CREATE %sINDEX%s %s ON %s '; - if ($index->getType() === self::GIN_INDEX_TYPE) { - $createIndexSentence .= ' USING ' . $index->getType() . '(%s) %s;'; - } else { - $createIndexSentence .= '(%s)%s%s;'; + // Build USING clause for access method types (gin, gist, spgist, brin, hash) + $indexType = $index->getType(); + $usingClause = ''; + if (in_array($indexType, self::ACCESS_METHOD_TYPES, true)) { + $usingClause = ' USING ' . $indexType; } + $where = (string)$index->getWhere(); if ($where) { $where = ' WHERE ' . $where; } return sprintf( - $createIndexSentence, - $index->getType() === Index::UNIQUE ? 'UNIQUE ' : '', + 'CREATE %sINDEX%s %s ON %s%s (%s)%s%s;', + $indexType === Index::UNIQUE ? 'UNIQUE ' : '', $index->getConcurrently() ? ' CONCURRENTLY' : '', $this->quoteColumnName((string)$indexName), $this->quoteTableName($tableName), + $usingClause, implode(',', $columnNames), $includedColumns, $where, diff --git a/src/Db/Table/Index.php b/src/Db/Table/Index.php index f1ab782b..e43f0cf2 100644 --- a/src/Db/Table/Index.php +++ b/src/Db/Table/Index.php @@ -36,6 +36,46 @@ class Index extends DatabaseIndex */ public const FULLTEXT = 'fulltext'; + /** + * PostgreSQL index access method: Generalized Inverted Index. + * Useful for full-text search, arrays, and JSONB columns. + * + * @var string + */ + public const GIN = 'gin'; + + /** + * PostgreSQL index access method: Generalized Search Tree. + * Useful for geometric data, range types, and full-text search. + * + * @var string + */ + public const GIST = 'gist'; + + /** + * PostgreSQL index access method: Space-Partitioned GiST. + * Useful for data with natural clustering like IP addresses or phone numbers. + * + * @var string + */ + public const SPGIST = 'spgist'; + + /** + * PostgreSQL index access method: Block Range Index. + * Highly efficient for large, naturally-ordered tables like time-series data. + * + * @var string + */ + public const BRIN = 'brin'; + + /** + * PostgreSQL index access method: Hash index. + * Handles simple equality comparisons. Rarely needed since B-tree handles equality efficiently. + * + * @var string + */ + public const HASH = 'hash'; + /** * Constructor * @@ -47,6 +87,7 @@ class Index extends DatabaseIndex * @param array|null $include The included columns for covering indexes. * @param ?string $where The where clause for partial indexes. * @param bool $concurrent Whether to create the index concurrently. + * @param array|null $opclass The operator class for each column (PostgreSQL). */ public function __construct( protected string $name = '', @@ -57,6 +98,7 @@ public function __construct( protected ?array $include = null, protected ?string $where = null, protected bool $concurrent = false, + protected ?array $opclass = null, ) { } @@ -149,6 +191,34 @@ public function getConcurrently(): bool return $this->concurrent; } + /** + * Set the operator class for index columns. + * + * Operator classes specify which operators the index can use. This is primarily + * useful in PostgreSQL for specialized index types like GiST with trigram support. + * + * Example: ['column_name' => 'gist_trgm_ops'] + * + * @param array $opclass Map of column names to operator classes. + * @return $this + */ + public function setOpclass(array $opclass) + { + $this->opclass = $opclass; + + return $this; + } + + /** + * Get the operator class configuration for index columns. + * + * @return array|null + */ + public function getOpclass(): ?array + { + return $this->opclass; + } + /** * Utility method that maps an array of index options to this object's methods. * @@ -159,7 +229,7 @@ public function getConcurrently(): bool public function setOptions(array $options) { // Valid Options - $validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where']; + $validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'opclass']; foreach ($options as $option => $value) { if (!in_array($option, $validOptions, true)) { throw new RuntimeException(sprintf('"%s" is not a valid index option.', $option)); diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 58c03871..239f7e2e 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -1493,6 +1493,152 @@ public function testDropIndexByNameWithSchema() $this->adapter->dropSchema('schema1'); } + public function testAddGistIndex(): void + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('data', 'text') + ->save(); + + $table->addIndex('data', ['type' => 'gist', 'opclass' => ['data' => 'gist_trgm_ops']]) + ->save(); + + $this->assertTrue($table->hasIndex('data')); + + // Verify the index uses the GIST access method + $rows = $this->adapter->fetchAll( + "SELECT am.amname as access_method + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_am am ON am.oid = c.relam + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = 'table1' AND c.relname = 'table1_data'" + ); + $this->assertCount(1, $rows); + $this->assertEquals('gist', $rows[0]['access_method']); + } + + public function testAddGinIndex(): void + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('tags', 'jsonb') + ->save(); + + $table->addIndex('tags', ['type' => 'gin']) + ->save(); + + $this->assertTrue($table->hasIndex('tags')); + + // Verify the index uses the GIN access method + $rows = $this->adapter->fetchAll( + "SELECT am.amname as access_method + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_am am ON am.oid = c.relam + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = 'table1' AND c.relname = 'table1_tags'" + ); + $this->assertCount(1, $rows); + $this->assertEquals('gin', $rows[0]['access_method']); + } + + public function testAddBrinIndex(): void + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('created_at', 'timestamp') + ->save(); + + $table->addIndex('created_at', ['type' => 'brin']) + ->save(); + + $this->assertTrue($table->hasIndex('created_at')); + + // Verify the index uses the BRIN access method + $rows = $this->adapter->fetchAll( + "SELECT am.amname as access_method + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_am am ON am.oid = c.relam + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = 'table1' AND c.relname = 'table1_created_at'" + ); + $this->assertCount(1, $rows); + $this->assertEquals('brin', $rows[0]['access_method']); + } + + public function testAddHashIndex(): void + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('session_id', 'string', ['limit' => 64]) + ->save(); + + $table->addIndex('session_id', ['type' => 'hash']) + ->save(); + + $this->assertTrue($table->hasIndex('session_id')); + + // Verify the index uses the HASH access method + $rows = $this->adapter->fetchAll( + "SELECT am.amname as access_method + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_am am ON am.oid = c.relam + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = 'table1' AND c.relname = 'table1_session_id'" + ); + $this->assertCount(1, $rows); + $this->assertEquals('hash', $rows[0]['access_method']); + } + + public function testAddSpgistIndex(): void + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('data', 'text') + ->save(); + + $table->addIndex('data', ['type' => 'spgist', 'opclass' => ['data' => 'text_ops']]) + ->save(); + + $this->assertTrue($table->hasIndex('data')); + + // Verify the index uses the SP-GIST access method + $rows = $this->adapter->fetchAll( + "SELECT am.amname as access_method + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_am am ON am.oid = c.relam + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = 'table1' AND c.relname = 'table1_data'" + ); + $this->assertCount(1, $rows); + $this->assertEquals('spgist', $rows[0]['access_method']); + } + + public function testAddIndexWithOpclass(): void + { + $table = new Table('table1', [], $this->adapter); + $table->addColumn('name', 'string') + ->save(); + + $table->addIndex('name', [ + 'type' => 'gist', + 'opclass' => ['name' => 'gist_trgm_ops'], + ])->save(); + + $this->assertTrue($table->hasIndex('name')); + + // Verify the index was created with the correct access method + $rows = $this->adapter->fetchAll( + "SELECT am.amname as access_method + FROM pg_index i + JOIN pg_class c ON c.oid = i.indexrelid + JOIN pg_am am ON am.oid = c.relam + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = 'table1' AND c.relname = 'table1_name'" + ); + $this->assertCount(1, $rows); + $this->assertEquals('gist', $rows[0]['access_method']); + } + public function testAddForeignKey() { $refTable = new Table('ref_table', [], $this->adapter); From cc3196e6f87e51eb75b67ce11a4fc38b40d297a6 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 15 Mar 2026 02:10:23 +0100 Subject: [PATCH 11/12] Fix CI failures: add trailing commas and fix tests for missing pg_trgm extension --- .../Db/Adapter/PostgresAdapterTest.php | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 239f7e2e..43b2cf25 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -1495,14 +1495,16 @@ public function testDropIndexByNameWithSchema() public function testAddGistIndex(): void { + // GiST indexes on text columns require an operator class. + // We use inet type which has built-in GiST support. $table = new Table('table1', [], $this->adapter); - $table->addColumn('data', 'text') + $table->addColumn('ip_range', 'inet') ->save(); - $table->addIndex('data', ['type' => 'gist', 'opclass' => ['data' => 'gist_trgm_ops']]) + $table->addIndex('ip_range', ['type' => 'gist']) ->save(); - $this->assertTrue($table->hasIndex('data')); + $this->assertTrue($table->hasIndex('ip_range')); // Verify the index uses the GIST access method $rows = $this->adapter->fetchAll( @@ -1511,7 +1513,7 @@ public function testAddGistIndex(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_data'" + WHERE t.relname = 'table1' AND c.relname = 'table1_ip_range'", ); $this->assertCount(1, $rows); $this->assertEquals('gist', $rows[0]['access_method']); @@ -1535,7 +1537,7 @@ public function testAddGinIndex(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_tags'" + WHERE t.relname = 'table1' AND c.relname = 'table1_tags'", ); $this->assertCount(1, $rows); $this->assertEquals('gin', $rows[0]['access_method']); @@ -1559,7 +1561,7 @@ public function testAddBrinIndex(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_created_at'" + WHERE t.relname = 'table1' AND c.relname = 'table1_created_at'", ); $this->assertCount(1, $rows); $this->assertEquals('brin', $rows[0]['access_method']); @@ -1583,7 +1585,7 @@ public function testAddHashIndex(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_session_id'" + WHERE t.relname = 'table1' AND c.relname = 'table1_session_id'", ); $this->assertCount(1, $rows); $this->assertEquals('hash', $rows[0]['access_method']); @@ -1591,6 +1593,7 @@ public function testAddHashIndex(): void public function testAddSpgistIndex(): void { + // SP-GiST indexes on text require the text_ops operator class $table = new Table('table1', [], $this->adapter); $table->addColumn('data', 'text') ->save(); @@ -1607,7 +1610,7 @@ public function testAddSpgistIndex(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_data'" + WHERE t.relname = 'table1' AND c.relname = 'table1_data'", ); $this->assertCount(1, $rows); $this->assertEquals('spgist', $rows[0]['access_method']); @@ -1615,6 +1618,14 @@ public function testAddSpgistIndex(): void public function testAddIndexWithOpclass(): void { + // Test opclass with GiST using pg_trgm extension + // Skip if extension is not available + try { + $this->adapter->execute('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + } catch (\Exception $e) { + $this->markTestSkipped('pg_trgm extension is not available'); + } + $table = new Table('table1', [], $this->adapter); $table->addColumn('name', 'string') ->save(); @@ -1633,7 +1644,7 @@ public function testAddIndexWithOpclass(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_name'" + WHERE t.relname = 'table1' AND c.relname = 'table1_name'", ); $this->assertCount(1, $rows); $this->assertEquals('gist', $rows[0]['access_method']); From 40da6d3789049d96e1b44ed1f2a0714b64efee43 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 15 Mar 2026 02:14:29 +0100 Subject: [PATCH 12/12] Fix GiST test to use int4range type and add Exception import --- .../Db/Adapter/PostgresAdapterTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 43b2cf25..34e3513b 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -8,6 +8,7 @@ use Cake\Console\TestSuite\StubConsoleOutput; use Cake\Database\Connection; use Cake\Datasource\ConnectionManager; +use Exception; use InvalidArgumentException; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Adapter\PostgresAdapter; @@ -1495,16 +1496,15 @@ public function testDropIndexByNameWithSchema() public function testAddGistIndex(): void { - // GiST indexes on text columns require an operator class. - // We use inet type which has built-in GiST support. - $table = new Table('table1', [], $this->adapter); - $table->addColumn('ip_range', 'inet') - ->save(); + // GiST indexes require specific data types with GiST support. + // We use int4range which has built-in GiST support in PostgreSQL. + $this->adapter->execute('CREATE TABLE table1 (id SERIAL PRIMARY KEY, int_range int4range)'); - $table->addIndex('ip_range', ['type' => 'gist']) + $table = new Table('table1', [], $this->adapter); + $table->addIndex('int_range', ['type' => 'gist']) ->save(); - $this->assertTrue($table->hasIndex('ip_range')); + $this->assertTrue($table->hasIndex('int_range')); // Verify the index uses the GIST access method $rows = $this->adapter->fetchAll( @@ -1513,7 +1513,7 @@ public function testAddGistIndex(): void JOIN pg_class c ON c.oid = i.indexrelid JOIN pg_am am ON am.oid = c.relam JOIN pg_class t ON t.oid = i.indrelid - WHERE t.relname = 'table1' AND c.relname = 'table1_ip_range'", + WHERE t.relname = 'table1' AND c.relname = 'table1_int_range'", ); $this->assertCount(1, $rows); $this->assertEquals('gist', $rows[0]['access_method']); @@ -1622,7 +1622,7 @@ public function testAddIndexWithOpclass(): void // Skip if extension is not available try { $this->adapter->execute('CREATE EXTENSION IF NOT EXISTS pg_trgm'); - } catch (\Exception $e) { + } catch (Exception $e) { $this->markTestSkipped('pg_trgm extension is not available'); }