From 9d74ef7f224b963177afed9eeeaf0f17e9a3a261 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 7 Mar 2026 05:00:26 +0100 Subject: [PATCH 01/72] Add fixed option for binary column type (#1014) Add support for the `fixed` attribute on binary columns to distinguish between fixed-length BINARY and variable-length VARBINARY types. This mirrors cakephp/cakephp#19207. * Add test cases for fixed option on binary columns Tests cover: - Column class getter/setter for fixed option - Column::toArray() including fixed in output - Column::setOptions() accepting fixed option - MigrationHelper::getColumnOption() including/excluding fixed - MysqlAdapter creating BINARY vs VARBINARY based on fixed option * Add column type assertions to binary fixed test --- src/Db/Adapter/MysqlAdapter.php | 3 ++ src/Db/Table/Column.php | 32 ++++++++++++ src/View/Helper/MigrationHelper.php | 3 +- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 49 +++++++++++++++++ tests/TestCase/Db/Table/ColumnTest.php | 52 +++++++++++++++++++ .../View/Helper/MigrationHelperTest.php | 34 ++++++++++++ 6 files changed, 172 insertions(+), 1 deletion(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index c07cfe6f7..a8aa1e8c7 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -651,6 +651,9 @@ public function getColumns(string $tableName): array if ($record['onUpdate'] ?? false) { $column->setUpdate($record['onUpdate']); } + if ($record['fixed'] ?? false) { + $column->setFixed(true); + } $columns[] = $column; } diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index 98f28b313..e608560ee 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -117,6 +117,11 @@ class Column extends DatabaseColumn */ protected ?string $lock = null; + /** + * @var bool|null + */ + protected ?bool $fixed = null; + /** * Column constructor * @@ -772,6 +777,31 @@ public function getLock(): ?string return $this->lock; } + /** + * Sets whether field should use fixed-length storage (for binary columns). + * + * When true, binary columns will use BINARY(n) instead of VARBINARY(n). + * + * @param bool $fixed Fixed + * @return $this + */ + public function setFixed(bool $fixed) + { + $this->fixed = $fixed; + + return $this; + } + + /** + * Gets whether field should use fixed-length storage. + * + * @return bool|null + */ + public function getFixed(): ?bool + { + return $this->fixed; + } + /** * Gets all allowed options. Each option must have a corresponding `setFoo` method. * @@ -802,6 +832,7 @@ protected function getValidOptions(): array 'generated', 'algorithm', 'lock', + 'fixed', ]; } @@ -894,6 +925,7 @@ public function toArray(): array 'default' => $default, 'generated' => $this->getGenerated(), 'unsigned' => $this->getUnsigned(), + 'fixed' => $this->getFixed(), 'onUpdate' => $this->getUpdate(), 'collate' => $this->getCollation(), 'precision' => $precision, diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 3302e7e5c..75e04cbb0 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -389,6 +389,7 @@ public function getColumnOption(array $options): array 'scale', 'after', 'collate', + 'fixed', ]); $columnOptions = array_intersect_key($options, $wantedOptions); if (empty($columnOptions['comment'])) { @@ -495,7 +496,7 @@ public function attributes(TableSchemaInterface|string $table, string $column): 'comment', 'unsigned', 'signed', 'properties', 'autoIncrement', 'unique', - 'collate', + 'collate', 'fixed', ]; $attributes = []; diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 85aded715..1ab36b5c8 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -3541,4 +3541,53 @@ public function testCombinedPartitionAndColumnOperations(): void $this->assertCount(1, $rows); $this->assertEquals('A description', $rows[0]['description']); } + + public function testBinaryColumnWithFixedOption(): void + { + $table = new Table('binary_fixed_test', [], $this->adapter); + $table->addColumn('hash', 'binary', ['limit' => 20, 'fixed' => true]) + ->addColumn('data', 'binary', ['limit' => 20]) + ->save(); + + $this->assertTrue($this->adapter->hasColumn('binary_fixed_test', 'hash')); + $this->assertTrue($this->adapter->hasColumn('binary_fixed_test', 'data')); + + // Check that the fixed column is created as BINARY and the non-fixed as VARBINARY + $rows = $this->adapter->fetchAll('SHOW COLUMNS FROM binary_fixed_test'); + $hashColumn = null; + $dataColumn = null; + foreach ($rows as $row) { + if ($row['Field'] === 'hash') { + $hashColumn = $row; + } + if ($row['Field'] === 'data') { + $dataColumn = $row; + } + } + + $this->assertNotNull($hashColumn); + $this->assertNotNull($dataColumn); + $this->assertSame('binary(20)', $hashColumn['Type']); + $this->assertSame('varbinary(20)', $dataColumn['Type']); + + // Verify the fixed attribute is reflected back + $columns = $this->adapter->getColumns('binary_fixed_test'); + $hashCol = null; + $dataCol = null; + foreach ($columns as $col) { + if ($col->getName() === 'hash') { + $hashCol = $col; + } + if ($col->getName() === 'data') { + $dataCol = $col; + } + } + + $this->assertNotNull($hashCol); + $this->assertNotNull($dataCol); + $this->assertSame('binary', $hashCol->getType()); + $this->assertSame('binary', $dataCol->getType()); + $this->assertTrue($hashCol->getFixed()); + $this->assertNull($dataCol->getFixed()); + } } diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 0652149f3..2d5ccd1ae 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -275,4 +275,56 @@ public function testUnsignedConfigurationDoesNotAffectNonIntegerTypes(): void $decimalColumn->setName('price')->setType('decimal'); $this->assertFalse($decimalColumn->isUnsigned()); } + + public function testFixedOptionDefaultsToNull(): void + { + $column = new Column(); + $column->setName('data')->setType('binary'); + + $this->assertNull($column->getFixed()); + } + + public function testSetFixedTrue(): void + { + $column = new Column(); + $column->setName('hash')->setType('binary')->setFixed(true); + + $this->assertTrue($column->getFixed()); + } + + public function testSetFixedFalse(): void + { + $column = new Column(); + $column->setName('data')->setType('binary')->setFixed(false); + + $this->assertFalse($column->getFixed()); + } + + public function testSetOptionsWithFixed(): void + { + $column = new Column(); + $column->setName('hash')->setType('binary'); + $column->setOptions(['fixed' => true, 'limit' => 20]); + + $this->assertTrue($column->getFixed()); + $this->assertSame(20, $column->getLimit()); + } + + public function testToArrayIncludesFixed(): void + { + $column = new Column(); + $column->setName('hash')->setType('binary')->setFixed(true)->setLimit(20); + + $result = $column->toArray(); + $this->assertTrue($result['fixed']); + } + + public function testToArrayFixedNullByDefault(): void + { + $column = new Column(); + $column->setName('data')->setType('binary')->setLimit(20); + + $result = $column->toArray(); + $this->assertNull($result['fixed']); + } } diff --git a/tests/TestCase/View/Helper/MigrationHelperTest.php b/tests/TestCase/View/Helper/MigrationHelperTest.php index c78009360..fd2dfe91c 100644 --- a/tests/TestCase/View/Helper/MigrationHelperTest.php +++ b/tests/TestCase/View/Helper/MigrationHelperTest.php @@ -456,4 +456,38 @@ public function testGetColumnOptionConvertsCollateToCollation(): void $this->assertArrayHasKey('collation', $result, 'collation should be set from collate value'); $this->assertSame('en_US.UTF-8', $result['collation']); } + + /** + * Test that getColumnOption includes the fixed option for binary columns + */ + public function testGetColumnOptionIncludesFixed(): void + { + $options = [ + 'length' => 20, + 'null' => true, + 'default' => null, + 'fixed' => true, + ]; + + $result = $this->helper->getColumnOption($options); + + $this->assertArrayHasKey('fixed', $result); + $this->assertTrue($result['fixed']); + } + + /** + * Test that getColumnOption excludes fixed when not set + */ + public function testGetColumnOptionExcludesFixedWhenNotSet(): void + { + $options = [ + 'length' => 20, + 'null' => true, + 'default' => null, + ]; + + $result = $this->helper->getColumnOption($options); + + $this->assertArrayNotHasKey('fixed', $result); + } } From ca1b74e523c105a12aa35e6c61d196b6997742f8 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 7 Mar 2026 19:21:57 +0100 Subject: [PATCH 02/72] Fix misleading next steps message in upgrade command (#1037) Only show 'Set legacyTables => false' step after tables have been dropped, as the upgrade command becomes unavailable once this config is set. Fixes #1036 --- src/Command/UpgradeCommand.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index 5897c4589..dece3c095 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -156,10 +156,13 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $io->success('Upgrade complete!'); $io->out(''); $io->out('Next steps:'); - $io->out(' 1. Set \'Migrations\' => [\'legacyTables\' => false] in your config'); - $io->out(' 2. Test your application'); - if (!$dropTables) { - $io->out(' 3. Optionally drop the empty phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)'); + if ($dropTables) { + $io->out(' 1. Set \'Migrations\' => [\'legacyTables\' => false] in your config'); + $io->out(' 2. Test your application'); + } else { + $io->out(' 1. Test your application'); + $io->out(' 2. Drop the phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)'); + $io->out(' 3. Set \'Migrations\' => [\'legacyTables\' => false] in your config'); } } else { $io->out(''); From 8d27e00a7ea0db8bae4d808fd6bcc2847bab40fc Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 7 Mar 2026 19:34:14 +0100 Subject: [PATCH 03/72] Fix upgrade command not matching plugins with slashes (#1039) * Fix upgrade command not matching plugins with slashes When upgrading from legacy phinxlog tables, the plugin name was being derived by camelizing the table prefix (e.g. cake_d_c_users -> CakeDCUsers). This didn't work for plugins with slashes in their names like CakeDC/Users since the slash was replaced with underscore during table name generation. This fix builds a map of loaded plugin names to their expected table prefixes, allowing proper matching of plugins like CakeDC/Users. Fixes #1038 * Fix test for SQL Server compatibility --- src/Command/UpgradeCommand.php | 34 ++++++++++- tests/TestCase/Command/UpgradeCommandTest.php | 61 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index dece3c095..140eb02f2 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -17,6 +17,7 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Core\Plugin; use Cake\Database\Connection; use Cake\Database\Exception\QueryException; use Cake\Datasource\ConnectionManager; @@ -184,13 +185,24 @@ protected function findLegacyTables(Connection $connection): array $tables = $schema->listTables(); $legacyTables = []; + // Build a map of expected table prefixes to plugin names for loaded plugins + // This allows matching plugins with special characters like CakeDC/Users + $pluginPrefixMap = $this->buildPluginPrefixMap(); + foreach ($tables as $table) { if ($table === 'phinxlog') { $legacyTables[$table] = null; } elseif (str_ends_with($table, '_phinxlog')) { // Extract plugin name from table name $prefix = substr($table, 0, -9); // Remove '_phinxlog' - $plugin = Inflector::camelize($prefix); + + // Try to match against loaded plugins first + if (isset($pluginPrefixMap[$prefix])) { + $plugin = $pluginPrefixMap[$prefix]; + } else { + // Fall back to camelizing the prefix + $plugin = Inflector::camelize($prefix); + } $legacyTables[$table] = $plugin; } } @@ -198,6 +210,26 @@ protected function findLegacyTables(Connection $connection): array return $legacyTables; } + /** + * Build a map of table prefixes to plugin names for all loaded plugins. + * + * This handles plugins with special characters like CakeDC/Users where + * the table prefix is cake_d_c_users but the plugin name is CakeDC/Users. + * + * @return array Map of table prefix => plugin name + */ + protected function buildPluginPrefixMap(): array + { + $map = []; + foreach (Plugin::loaded() as $plugin) { + $prefix = Inflector::underscore($plugin); + $prefix = str_replace(['\\', '/', '.'], '_', $prefix); + $map[$prefix] = $plugin; + } + + return $map; + } + /** * Check if a table exists. * diff --git a/tests/TestCase/Command/UpgradeCommandTest.php b/tests/TestCase/Command/UpgradeCommandTest.php index ceddd9537..c7900bd7a 100644 --- a/tests/TestCase/Command/UpgradeCommandTest.php +++ b/tests/TestCase/Command/UpgradeCommandTest.php @@ -166,4 +166,65 @@ public function testExecuteWithMigrations(): void $this->assertCount(1, $rows); } + + /** + * Test that plugins with slashes (like CakeDC/Users) are correctly identified + * during upgrade from legacy phinxlog tables. + */ + public function testExecuteWithSlashInPluginName(): void + { + Configure::write('Migrations.legacyTables', true); + + // Create the plugin's phinxlog table using the adapter for cross-database compatibility + $config = ConnectionManager::getConfig('test'); + $environment = new Environment('default', [ + 'connection' => 'test', + 'database' => $config['database'], + 'migration_table' => 'cake_d_c_users_phinxlog', + ]); + $adapter = $environment->getAdapter(); + try { + $adapter->createSchemaTable(); + } catch (Exception $e) { + // Table probably exists + } + + // Insert a migration record + $adapter->getInsertBuilder() + ->insert(['version', 'migration_name', 'breakpoint']) + ->into('cake_d_c_users_phinxlog') + ->values([ + 'version' => '20250118143003', + 'migration_name' => 'SlashPluginMigration', + 'breakpoint' => 0, + ]) + ->execute(); + + // Load a fake plugin with a slash in the name using loadPlugins + // which properly integrates with the console application + $this->loadPlugins(['CakeDC/Users' => ['path' => TMP]]); + + try { + $this->exec('migrations upgrade -c test'); + $this->assertExitSuccess(); + + $this->assertOutputContains('cake_d_c_users_phinxlog (CakeDC/Users)'); + + // Verify the plugin column has the correct value with slash + $rows = $this->getAdapter()->getSelectBuilder() + ->select(['version', 'migration_name', 'plugin']) + ->from('cake_migrations') + ->where(['migration_name' => 'SlashPluginMigration']) + ->all(); + + $this->assertCount(1, $rows); + $this->assertSame('CakeDC/Users', $rows[0]['plugin']); + } finally { + // Cleanup + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + $connection->execute('DROP TABLE ' . $connection->getDriver()->quoteIdentifier('cake_d_c_users_phinxlog')); + $this->removePlugins(['CakeDC/Users']); + } + } } From 612764c7d69601b78b8efb43de861fc97cce1b03 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 7 Mar 2026 19:38:22 +0100 Subject: [PATCH 04/72] Add TYPE_BIT constant to AdapterInterface (#1013) * Add TYPE_BIT constant to AdapterInterface Adds the TYPE_BIT constant mapping to TableSchemaInterface::TYPE_BIT for consistency with the core BIT type support added in cakephp/cakephp#19223. This allows migration users to reference AdapterInterface::TYPE_BIT when defining BIT columns in MySQL migrations. * Bump cakephp/database constraint to ^5.3.2 for TYPE_BIT support --- composer.json | 2 +- src/Db/Adapter/AdapterInterface.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0d0597f15..1c3779aa3 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "require": { "php": ">=8.2", "cakephp/cache": "^5.3.0", - "cakephp/database": "^5.3.0", + "cakephp/database": "^5.3.2", "cakephp/orm": "^5.3.0" }, "require-dev": { diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 74d43406a..0dad1ca37 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -63,6 +63,7 @@ interface AdapterInterface // only for mysql so far public const TYPE_YEAR = TableSchemaInterface::TYPE_YEAR; + public const TYPE_BIT = TableSchemaInterface::TYPE_BIT; // only for postgresql so far public const TYPE_CIDR = TableSchemaInterface::TYPE_CIDR; From 4085be7f02c50042a036cb408dc484d6ae3c89cc Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 8 Mar 2026 22:00:57 +0100 Subject: [PATCH 05/72] 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 a8aa1e8c7..01dfe8c16 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 9145e0cb8..f78ff76b0 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 06/72] 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 93edeb6d8..a92240322 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 07/72] 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 5c6e8dd7f..bd3532e16 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 08/72] 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 bd3532e16..5c6e8dd7f 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 09/72] 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 01dfe8c16..694f821b1 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 878f81ee9..b0e61d220 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 f78ff76b0..82a3812df 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 e804d5908..67f08c00c 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 10/72] 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 75e04cbb0..b0014f7c8 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 11/72] 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 23ab2e113..d413bff46 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 9cdbadae70d5bb6bec07585d47a2332140827bac Mon Sep 17 00:00:00 2001 From: Jamison Bryant Date: Wed, 11 Mar 2026 15:59:28 -0400 Subject: [PATCH 12/72] Raise PHPStan level to 8 with null safety fixes (#1044) * add using when changing column type to json (#1031) * Fix CI failures on MySQL/MariaDB (#1034) 1. Handle uninitialized Column::$fixed property in BakeMigrationDiffCommand When TableSchema::getColumn() is called on cached/serialized schema objects, the Column::$fixed property may not be initialized, causing an Error. Added safeGetColumn() helper that catches this error and uses reflection to initialize uninitialized properties before retry. 2. Fix CURRENT_TIMESTAMP test assertion for MySQL/MariaDB Different versions of MySQL and MariaDB return CURRENT_TIMESTAMP in different formats (CURRENT_TIMESTAMP, current_timestamp(), CURRENT_TIMESTAMP()). Changed the test to use a regex that accepts all valid formats case-insensitively. Fixes #1033 * Fix TEXT column variants not round-tripping correctly (#1032) When using migration_diff, TEXT column variants (TINYTEXT, MEDIUMTEXT, LONGTEXT) were not being properly mapped back from the database. The BLOB type handling already used rawType to distinguish BLOB variants, but TEXT variants were missing equivalent handling. This adds similar rawType-based mapping for TEXT columns in mapColumnType() and includes round-trip tests. Fixes #1029 * Add fixed option for binary column type (#1014) Add support for the `fixed` attribute on binary columns to distinguish between fixed-length BINARY and variable-length VARBINARY types. This mirrors cakephp/cakephp#19207. * Add test cases for fixed option on binary columns Tests cover: - Column class getter/setter for fixed option - Column::toArray() including fixed in output - Column::setOptions() accepting fixed option - MigrationHelper::getColumnOption() including/excluding fixed - MysqlAdapter creating BINARY vs VARBINARY based on fixed option * Add column type assertions to binary fixed test * Fix misleading next steps message in upgrade command (#1037) Only show 'Set legacyTables => false' step after tables have been dropped, as the upgrade command becomes unavailable once this config is set. Fixes #1036 * Fix upgrade command not matching plugins with slashes (#1039) * Fix upgrade command not matching plugins with slashes When upgrading from legacy phinxlog tables, the plugin name was being derived by camelizing the table prefix (e.g. cake_d_c_users -> CakeDCUsers). This didn't work for plugins with slashes in their names like CakeDC/Users since the slash was replaced with underscore during table name generation. This fix builds a map of loaded plugin names to their expected table prefixes, allowing proper matching of plugins like CakeDC/Users. Fixes #1038 * Fix test for SQL Server compatibility * Add TYPE_BIT constant to AdapterInterface (#1013) * Add TYPE_BIT constant to AdapterInterface Adds the TYPE_BIT constant mapping to TableSchemaInterface::TYPE_BIT for consistency with the core BIT type support added in cakephp/cakephp#19223. This allows migration users to reference AdapterInterface::TYPE_BIT when defining BIT columns in MySQL migrations. * Bump cakephp/database constraint to ^5.3.2 for TYPE_BIT support * Bump PHPStan level +1 * Make props non-nullable * Add null guards for getConstraint() in BakeMigrationDiffCommand * Fix nullable return types in Column and ForeignKey Column::getNull() now coalesces the nullable bool property to false. ForeignKey::getOnDelete()/getOnUpdate() guard against null from getDelete()/getUpdate() before calling mapAction(). * Fix null safety in Db adapters and domain classes Cast preg_replace results to string, coalesce nullable getColumns() returns, cast nullable getReferencedTable()/getName() at call sites, and use null-safe operator for optional ConsoleIo. * Fix remaining PHPStan level 8 null safety issues Add null guards for constraint diff, fail-fast for null column type, fix import order, and remaining null safety in BaseSeed, ColumnParser, TableFinder, and MigrationHelper. * Replace assert with RuntimeException in AbstractAdapter::getConnection() Assertions can be disabled in production, which would allow a null connection to slip through silently. Throw a RuntimeException instead to fail fast in all environments. * Use truthiness checks for optional nullable getters Replace (string) casts with assign-then-check pattern for optional values that can legitimately be null: Index::getWhere(), ForeignKey::getName(), Index::getName(), Column::getGenerated(). This avoids silently converting null to empty string. * Throw on null for required Column::getName() and FK::getReferencedTable() Replace (string) casts with explicit null checks and InvalidArgumentException throws for values that must be present: Column::getName() in SQL generation and ForeignKey::getReferencedTable() in FK definition builders. A column without a name or a foreign key without a referenced table are always programming errors that should fail fast rather than silently produce broken SQL. * Narrow ForeignKey::getColumns() return type and remove null guards Override getColumns() in Migrations ForeignKey to return array instead of the parent's ?array. The $columns property is always initialized as [] in the constructor, making null unreachable. This is a covariant return type narrowing, same pattern used for Column::getNull(). Removes the now-unnecessary ?? [] fallbacks from all 6 call sites across the adapter and plan classes. * Narrow Column::getName() return type and remove dead null checks Override getName() in Migrations Column to return string instead of the parent's ?string. The $name property is typed as string (not ?string) in the constructor, making null unreachable. This is a covariant return type narrowing, same pattern used for ForeignKey::getColumns(). Removes now-unnecessary null checks across adapter/action files and the dead null guard in Table::setPrimaryKey(). * Add null guards for column diff loop in BakeMigrationDiffCommand Use early-continue pattern to narrow nullable safeGetColumn() return types before array operations, fixing PHPStan level 8 errors. * Cast nullable getName() to string for drop FK/index instructions * Fix phpcs coding standard violations Remove unused InvalidArgumentException import from RecordingAdapter, extract assignment out of if condition in PostgresAdapter, and remove blank line before closing brace in ForeignKeyTest. * Consolidate null guard conditions in BakeMigrationDiffCommand * Fix inverted condition in ChangeColumn name fallback * Accept null in Column::setFixed() for snapshot compatibility Generated migration snapshots can emit 'fixed' => null for binary columns, which causes a TypeError since setFixed() only accepted bool. Widen the parameter to ?bool to match the property and getter types. Fixes #1046. --------- Co-authored-by: Matthias Wirtz Co-authored-by: Mark Scherer Co-authored-by: Jamison Bryant --- phpstan.neon | 2 +- src/BaseSeed.php | 4 +-- src/Command/BakeMigrationDiffCommand.php | 32 ++++++++++++----- src/Command/BakeSimpleMigrationCommand.php | 8 ++--- src/Db/Action/ChangeColumn.php | 2 +- src/Db/Adapter/AbstractAdapter.php | 13 ++++--- src/Db/Adapter/MysqlAdapter.php | 11 ++++-- src/Db/Adapter/PostgresAdapter.php | 40 +++++++++++++--------- src/Db/Adapter/RecordingAdapter.php | 2 +- src/Db/Adapter/SqliteAdapter.php | 30 +++++++++------- src/Db/Adapter/SqlserverAdapter.php | 38 +++++++++++++------- src/Db/Adapter/TimedOutputAdapter.php | 2 +- src/Db/Table.php | 10 +++--- src/Db/Table/Column.php | 13 ++++--- src/Db/Table/ForeignKey.php | 21 ++++++++++-- src/Util/ColumnParser.php | 2 +- src/Util/TableFinder.php | 5 +-- src/View/Helper/MigrationHelper.php | 2 +- tests/TestCase/Db/Table/ColumnTest.php | 11 ++++++ tests/TestCase/Db/Table/TableTest.php | 9 +++++ 20 files changed, 175 insertions(+), 82 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index d3803bdec..b35bf0118 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,7 +2,7 @@ includes: - phpstan-baseline.neon parameters: - level: 7 + level: 8 paths: - src/ bootstrapFiles: diff --git a/src/BaseSeed.php b/src/BaseSeed.php index 146abc4e1..ab7c93042 100644 --- a/src/BaseSeed.php +++ b/src/BaseSeed.php @@ -278,8 +278,8 @@ protected function runCall(string $seeder, array $options = []): void $options += [ 'connection' => $connection, - 'plugin' => $pluginName ?? $config['plugin'], - 'source' => $config['source'], + 'plugin' => $pluginName ?? ($config !== null ? $config['plugin'] : null), + 'source' => $config !== null ? $config['source'] : null, ]; $factory = new ManagerFactory([ 'connection' => $options['connection'], diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index d0490dafe..a67ef3c51 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -279,7 +279,15 @@ protected function getColumns(): void // changes in columns meta-data foreach ($currentColumns as $columnName) { $column = $this->safeGetColumn($currentSchema, $columnName); + if ($column === null || !in_array($columnName, $oldColumns, true)) { + continue; + } + $oldColumn = $this->safeGetColumn($this->dumpSchema[$table], $columnName); + if ($oldColumn === null) { + continue; + } + unset( $column['collate'], $column['fixed'], @@ -287,10 +295,7 @@ protected function getColumns(): void $oldColumn['fixed'], ); - if ( - in_array($columnName, $oldColumns, true) && - $column !== $oldColumn - ) { + if ($column !== $oldColumn) { $changedAttributes = array_diff_assoc($column, $oldColumn); foreach (['type', 'length', 'null', 'default'] as $attribute) { @@ -385,9 +390,10 @@ protected function getConstraints(): void // brand new constraints $addedConstraints = array_diff($currentConstraints, $oldConstraints); foreach ($addedConstraints as $constraintName) { - $this->templateData[$table]['constraints']['add'][$constraintName] = - $currentSchema->getConstraint($constraintName); $constraint = $currentSchema->getConstraint($constraintName); + if ($constraint === null) { + continue; + } if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { $this->templateData[$table]['constraints']['add'][$constraintName] = $constraint; } else { @@ -399,13 +405,18 @@ protected function getConstraints(): void // if present in both, check if they are the same : if not, remove the old one and add the new one foreach ($currentConstraints as $constraintName) { $constraint = $currentSchema->getConstraint($constraintName); + if ($constraint === null) { + continue; + } + $oldConstraint = $this->dumpSchema[$table]->getConstraint($constraintName); if ( in_array($constraintName, $oldConstraints, true) && - $constraint !== $this->dumpSchema[$table]->getConstraint($constraintName) + $constraint !== $oldConstraint ) { - $this->templateData[$table]['constraints']['remove'][$constraintName] = - $this->dumpSchema[$table]->getConstraint($constraintName); + if ($oldConstraint !== null) { + $this->templateData[$table]['constraints']['remove'][$constraintName] = $oldConstraint; + } $this->templateData[$table]['constraints']['add'][$constraintName] = $constraint; } @@ -415,6 +426,9 @@ protected function getConstraints(): void $removedConstraints = array_diff($oldConstraints, $currentConstraints); foreach ($removedConstraints as $constraintName) { $constraint = $this->dumpSchema[$table]->getConstraint($constraintName); + if ($constraint === null) { + continue; + } if ($constraint['type'] === TableSchema::CONSTRAINT_FOREIGN) { $this->templateData[$table]['constraints']['remove'][$constraintName] = $constraint; } else { diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php index 622b61fcc..08b703e32 100644 --- a/src/Command/BakeSimpleMigrationCommand.php +++ b/src/Command/BakeSimpleMigrationCommand.php @@ -52,16 +52,16 @@ abstract class BakeSimpleMigrationCommand extends SimpleBakeCommand /** * Console IO * - * @var \Cake\Console\ConsoleIo|null + * @var \Cake\Console\ConsoleIo */ - protected ?ConsoleIo $io = null; + protected ConsoleIo $io; /** * Arguments * - * @var \Cake\Console\Arguments|null + * @var \Cake\Console\Arguments */ - protected ?Arguments $args = null; + protected Arguments $args; /** * @inheritDoc diff --git a/src/Db/Action/ChangeColumn.php b/src/Db/Action/ChangeColumn.php index 267e30aa9..f4939ebd4 100644 --- a/src/Db/Action/ChangeColumn.php +++ b/src/Db/Action/ChangeColumn.php @@ -41,7 +41,7 @@ public function __construct(TableMetadata $table, string $columnName, Column $co $this->column = $column; // if the name was omitted use the existing column name - if ($column->getName() === null || strlen((string)$column->getName()) === 0) { + if (!$column->getName()) { $column->setName($columnName); } } diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 556a60787..367e89cc4 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -201,6 +201,9 @@ public function getConnection(): Connection $this->connection = $this->getOption('connection'); $this->connect(); } + if ($this->connection === null) { + throw new RuntimeException('Unable to establish database connection. Ensure a connection is configured.'); + } return $this->connection; } @@ -1687,17 +1690,19 @@ public function executeActions(TableMetadata $table, array $actions): void case $action instanceof DropForeignKey && $action->getForeignKey()->getName(): /** @var \Migrations\Db\Action\DropForeignKey $action */ + $fkName = (string)$action->getForeignKey()->getName(); $instructions->merge($this->getDropForeignKeyInstructions( $table->getName(), - (string)$action->getForeignKey()->getName(), + $fkName, )); break; case $action instanceof DropIndex && $action->getIndex()->getName(): /** @var \Migrations\Db\Action\DropIndex $action */ + $indexName = (string)$action->getIndex()->getName(); $instructions->merge($this->getDropIndexByNameInstructions( $table->getName(), - (string)$action->getIndex()->getName(), + $indexName, )); break; @@ -1720,7 +1725,7 @@ public function executeActions(TableMetadata $table, array $actions): void /** @var \Migrations\Db\Action\RemoveColumn $action */ $instructions->merge($this->getDropColumnInstructions( $table->getName(), - (string)$action->getColumn()->getName(), + $action->getColumn()->getName(), )); break; @@ -1728,7 +1733,7 @@ public function executeActions(TableMetadata $table, array $actions): void /** @var \Migrations\Db\Action\RenameColumn $action */ $instructions->merge($this->getRenameColumnInstructions( $table->getName(), - (string)$action->getColumn()->getName(), + $action->getColumn()->getName(), $action->getNewName(), )); break; diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index a8aa1e8c7..6dd6c0ba6 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -1197,8 +1197,9 @@ protected function getIndexSqlDefinition(Index $index): string protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string { $def = ''; - if ($foreignKey->getName()) { - $def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName()); + $name = $foreignKey->getName(); + if ($name) { + $def .= ' CONSTRAINT ' . $this->quoteColumnName($name); } $columnNames = []; foreach ($foreignKey->getColumns() as $column) { @@ -1209,7 +1210,11 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string foreach ($foreignKey->getReferencedColumns() as $column) { $refColumnNames[] = $this->quoteColumnName($column); } - $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . implode(',', $refColumnNames) . ')'; + $referencedTable = $foreignKey->getReferencedTable(); + if ($referencedTable === null) { + throw new InvalidArgumentException('Foreign key must have a referenced table.'); + } + $def .= ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . implode(',', $refColumnNames) . ')'; $onDelete = $foreignKey->getOnDelete(); if ($onDelete) { $def .= ' ON DELETE ' . $onDelete; diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 878f81ee9..899c1ee92 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -455,9 +455,9 @@ protected function getChangeColumnInstructions( $columnSql = $dialect->columnDefinitionSql($this->mapColumnData($newColumn->toArray())); // Remove the column name from $columnSql - $columnType = preg_replace('/^"?(?:[^"]+)"?\s+/', '', $columnSql); + $columnType = (string)preg_replace('/^"?(?:[^"]+)"?\s+/', '', $columnSql); // Remove generated clause - $columnType = preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', $columnType); + $columnType = (string)preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', $columnType); $sql = sprintf( 'ALTER COLUMN %s TYPE %s', @@ -483,10 +483,10 @@ protected function getChangeColumnInstructions( ); } // NULL and DEFAULT cannot be set while changing column type - $sql = preg_replace('/ NOT NULL/', '', $sql); - $sql = preg_replace('/ DEFAULT NULL/', '', $sql); + $sql = (string)preg_replace('/ NOT NULL/', '', $sql); + $sql = (string)preg_replace('/ DEFAULT NULL/', '', $sql); // If it is set, DEFAULT is the last definition - $sql = preg_replace('/DEFAULT .*/', '', $sql); + $sql = (string)preg_replace('/DEFAULT .*/', '', $sql); if ($newColumn->getType() === 'boolean') { $sql .= sprintf( ' USING (CASE WHEN %s IS NULL THEN NULL WHEN %s::int=0 THEN FALSE ELSE TRUE END)', @@ -505,11 +505,12 @@ protected function getChangeColumnInstructions( 'ALTER COLUMN %s', $quotedColumnName, ); - if ($newColumn->isIdentity() && $newColumn->getGenerated() !== null) { + $generated = $newColumn->getGenerated(); + if ($newColumn->isIdentity() && $generated !== null) { if ($column->isIdentity()) { - $sql .= sprintf(' SET GENERATED %s', (string)$newColumn->getGenerated()); + $sql .= sprintf(' SET GENERATED %s', $generated); } else { - $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', (string)$newColumn->getGenerated()); + $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', $generated); } } else { $sql .= ' DROP IDENTITY IF EXISTS'; @@ -546,12 +547,13 @@ protected function getChangeColumnInstructions( } // rename column - if ($columnName !== $newColumn->getName()) { + $newColumnName = $newColumn->getName(); + if ($columnName !== $newColumnName) { $instructions->addPostStep(sprintf( 'ALTER TABLE %s RENAME COLUMN %s TO %s', $this->quoteTableName($tableName), $quotedColumnName, - $this->quoteColumnName((string)$newColumn->getName()), + $this->quoteColumnName($newColumnName), )); } @@ -873,6 +875,7 @@ public function dropDatabase($name): void */ protected function getColumnCommentSqlDefinition(Column $column, string $tableName): string { + $columnName = $column->getName(); $comment = (string)$column->getComment(); // passing 'null' is to remove column comment $comment = strcasecmp($comment, 'NULL') !== 0 @@ -882,7 +885,7 @@ protected function getColumnCommentSqlDefinition(Column $column, string $tableNa return sprintf( 'COMMENT ON COLUMN %s.%s IS %s;', $this->quoteTableName($tableName), - $this->quoteColumnName((string)$column->getName()), + $this->quoteColumnName($columnName), $comment, ); } @@ -923,9 +926,10 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin } else { $createIndexSentence .= '(%s)%s%s;'; } - $where = (string)$index->getWhere(); - if ($where) { - $where = ' WHERE ' . $where; + $where = ''; + $whereClause = $index->getWhere(); + if ($whereClause) { + $where = ' WHERE ' . $whereClause; } return sprintf( @@ -956,9 +960,13 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta ); $columnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getColumns())); $refColumnList = implode(', ', array_map($this->quoteColumnName(...), $foreignKey->getReferencedColumns())); + $referencedTable = $foreignKey->getReferencedTable(); + if ($referencedTable === null) { + throw new InvalidArgumentException('Foreign key must have a referenced table.'); + } $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName) . ' FOREIGN KEY (' . $columnList . ')' . - ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . $refColumnList . ')'; + ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . $refColumnList . ')'; if ($foreignKey->getOnDelete()) { $def .= " ON DELETE {$foreignKey->getOnDelete()}"; } @@ -1327,7 +1335,7 @@ protected function getConflictClause( } $quotedConflictColumns = array_map($this->quoteColumnName(...), $conflictColumns); $updates = []; - foreach ($updateColumns as $column) { + foreach ($updateColumns ?? [] as $column) { $quotedColumn = $this->quoteColumnName($column); $updates[] = $quotedColumn . ' = EXCLUDED.' . $quotedColumn; } diff --git a/src/Db/Adapter/RecordingAdapter.php b/src/Db/Adapter/RecordingAdapter.php index aab4b41b3..9f1e90b82 100644 --- a/src/Db/Adapter/RecordingAdapter.php +++ b/src/Db/Adapter/RecordingAdapter.php @@ -89,7 +89,7 @@ public function getInvertedCommands(): Intent case $command instanceof RenameColumn: /** @var \Migrations\Db\Action\RenameColumn $command */ $column = clone $command->getColumn(); - $name = (string)$column->getName(); + $name = $column->getName(); $column->setName($command->getNewName()); $inverted->addAction(new RenameColumn($command->getTable(), $column, $name)); break; diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 9145e0cb8..2cd6894da 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -570,7 +570,7 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column return $state; } $finalColumnName = end($columns)->getName(); - $sql = preg_replace( + $sql = (string)preg_replace( sprintf( "/(%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+)([,)])/", $this->quoteColumnName((string)$finalColumnName), @@ -619,7 +619,7 @@ protected function getDeclaringSql(string $tableName): string $columnNamePattern = "\"$columnName\"|`$columnName`|\\[$columnName\\]|$columnName"; $columnNamePattern = "#([\(,]+\\s*)($columnNamePattern)(\\s)#iU"; - $sql = preg_replace_callback( + $sql = (string)preg_replace_callback( $columnNamePattern, function ($matches) use ($column) { return $matches[1] . $this->quoteColumnName($column['name']) . $matches[3]; @@ -631,7 +631,7 @@ function ($matches) use ($column) { $tableNamePattern = "\"$tableName\"|`$tableName`|\\[$tableName\\]|$tableName"; $tableNamePattern = "#^(CREATE TABLE)\s*($tableNamePattern)\s*(\()#Ui"; - $sql = preg_replace($tableNamePattern, "$1 `$tableName` $3", $sql, 1); + $sql = (string)preg_replace($tableNamePattern, "$1 `$tableName` $3", $sql, 1); return $sql; } @@ -1112,10 +1112,10 @@ protected function getChangeColumnInstructions(string $tableName, string $column { $instructions = $this->beginAlterByCopyTable($tableName); - $newColumnName = (string)$newColumn->getName(); + $newColumnName = $newColumn->getName(); $instructions->addPostStep(function ($state) use ($columnName, $newColumn) { $dialect = $this->getSchemaDialect(); - $sql = preg_replace( + $sql = (string)preg_replace( sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)), sprintf('%s$1', $dialect->columnDefinitionSql($newColumn->toArray())), (string)$state['createSQL'], @@ -1149,7 +1149,7 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa }); $instructions->addPostStep(function ($state) use ($columnName) { - $sql = preg_replace( + $sql = (string)preg_replace( sprintf("/%s\s\w+.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName))), '', (string)$state['createSQL'], @@ -1214,9 +1214,10 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index): $indexColumnArray[] = sprintf('%s ASC', $this->quoteColumnName($column)); } $indexColumns = implode(',', $indexColumnArray); - $where = (string)$index->getWhere(); - if ($where) { - $where = ' WHERE ' . $where; + $where = ''; + $whereClause = $index->getWhere(); + if ($whereClause) { + $where = ' WHERE ' . $whereClause; } $sql = sprintf( 'CREATE %s ON %s (%s)%s', @@ -1675,8 +1676,9 @@ public function getColumnTypes(): array protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string { $def = ''; - if ($foreignKey->getName()) { - $def .= ' CONSTRAINT ' . $this->quoteColumnName((string)$foreignKey->getName()); + $name = $foreignKey->getName(); + if ($name) { + $def .= ' CONSTRAINT ' . $this->quoteColumnName($name); } $columnNames = []; foreach ($foreignKey->getColumns() as $column) { @@ -1687,7 +1689,11 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey): string foreach ($foreignKey->getReferencedColumns() as $column) { $refColumnNames[] = $this->quoteColumnName($column); } - $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . implode(',', $refColumnNames) . ')'; + $referencedTable = $foreignKey->getReferencedTable(); + if ($referencedTable === null) { + throw new InvalidArgumentException('Foreign key must have a referenced table.'); + } + $def .= ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . implode(',', $refColumnNames) . ')'; if ($foreignKey->getOnDelete()) { $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); } diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index e804d5908..43ede29ca 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -213,8 +213,13 @@ protected function getChangeCommentInstructions(TableMetadata $table, ?string $n */ protected function getColumnCommentSqlDefinition(Column $column, ?string $tableName): string { + $columnName = $column->getName(); + if ($tableName === null) { + throw new InvalidArgumentException('Table name must be set.'); + } + // passing 'null' is to remove column comment - $currentComment = $this->getColumnComment((string)$tableName, $column->getName()); + $currentComment = $this->getColumnComment($tableName, $columnName); $comment = strcasecmp((string)$column->getComment(), 'NULL') !== 0 ? $this->quoteString((string)$column->getComment()) : '\'\''; $command = $currentComment === null ? 'sp_addextendedproperty' : 'sp_updateextendedproperty'; @@ -224,8 +229,8 @@ protected function getColumnCommentSqlDefinition(Column $column, ?string $tableN $command, $comment, $this->schema, - (string)$tableName, - (string)$column->getName(), + $tableName, + $columnName, ); } @@ -338,7 +343,7 @@ protected function parseDefault(?string $default): int|string|null $result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default); - if (strtoupper($result) === 'NULL') { + if (strtoupper((string)$result) === 'NULL') { $result = null; } elseif (is_numeric($result)) { $result = (int)$result; @@ -420,12 +425,13 @@ protected function getChangeDefault(string $tableName, Column $newColumn): Alter return $instructions; } + $newColumnName = $newColumn->getName(); $instructions->addPostStep(sprintf( 'ALTER TABLE %s ADD CONSTRAINT %s %s FOR %s', $this->quoteTableName($tableName), $constraintName, $default, - $this->quoteColumnName((string)$newColumn->getName()), + $this->quoteColumnName($newColumnName), )); return $instructions; @@ -455,14 +461,15 @@ protected function getChangeColumnInstructions(string $tableName, string $column $instructions = new AlterInstructions(); $dialect = $this->getSchemaDialect(); - if ($columnName !== $newColumn->getName()) { + $newColumnName = $newColumn->getName(); + if ($columnName !== $newColumnName) { $instructions->merge( - $this->getRenameColumnInstructions($tableName, $columnName, (string)$newColumn->getName()), + $this->getRenameColumnInstructions($tableName, $columnName, $newColumnName), ); } if ($changeDefault) { - $instructions->merge($this->getDropDefaultConstraint($tableName, (string)$newColumn->getName())); + $instructions->merge($this->getDropDefaultConstraint($tableName, $newColumnName)); } // Sqlserver doesn't support defaults @@ -475,7 +482,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column $dialect->columnDefinitionSql($columnData), ); $alterColumn = preg_replace('/DEFAULT NULL/', '', $alterColumn); - $instructions->addPostStep($alterColumn); + $instructions->addPostStep((string)$alterColumn); // change column comment if needed if ($newColumn->getComment()) { @@ -839,9 +846,10 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin $include = $index->getInclude(); $includedColumns = $include ? sprintf(' INCLUDE ([%s])', implode('],[', $include)) : ''; - $where = (string)$index->getWhere(); - if ($where) { - $where = ' WHERE ' . $where; + $where = ''; + $whereClause = $index->getWhere(); + if ($whereClause) { + $where = ' WHERE ' . $whereClause; } return sprintf( @@ -870,7 +878,11 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta $def = ' CONSTRAINT ' . $this->quoteColumnName($constraintName); $def .= ' FOREIGN KEY (' . $columnList . ')'; - $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . $refColumnList . ')'; + $referencedTable = $foreignKey->getReferencedTable(); + if ($referencedTable === null) { + throw new InvalidArgumentException('Foreign key must have a referenced table.'); + } + $def .= ' REFERENCES ' . $this->quoteTableName($referencedTable) . ' (' . $refColumnList . ')'; if ($foreignKey->getOnDelete()) { $def .= " ON DELETE {$foreignKey->getOnDelete()}"; } diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php index f5e11be62..fa5b47a19 100644 --- a/src/Db/Adapter/TimedOutputAdapter.php +++ b/src/Db/Adapter/TimedOutputAdapter.php @@ -78,7 +78,7 @@ function ($value) { return; } - $this->getIo()->verbose(' -- ' . $command); + $this->getIo()?->verbose(' -- ' . $command); } /** diff --git a/src/Db/Table.php b/src/Db/Table.php index aeeb09066..c83d7fd5f 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -358,19 +358,21 @@ public function addPrimaryKey(string|array $columns) */ public function addColumn(string|Column $columnName, ?string $type = null, array $options = []) { - assert($columnName instanceof Column || $type !== null); if ($columnName instanceof Column) { $action = new AddColumn($this->table, $columnName); + } elseif ($type === null) { + throw new InvalidArgumentException('Column type must not be null when column name is a string.'); } else { $action = new AddColumn($this->table, $this->getAdapter()->getColumnForType($columnName, $type, $options)); } // Delegate to Adapters to check column type - if (!$this->getAdapter()->isValidColumnType($action->getColumn())) { + $column = $action->getColumn(); + if (!$this->getAdapter()->isValidColumnType($column)) { throw new InvalidArgumentException(sprintf( 'An invalid column type "%s" was specified for column "%s".', - (string)$action->getColumn()->getType(), - (string)$action->getColumn()->getName(), + $column->getType(), + $column->getName(), )); } diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index e608560ee..750e74c4d 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -182,9 +182,12 @@ public function setName(string $name) /** * Gets the column name. * - * @return string|null + * Narrows the return type from the parent's ?string to string, + * since $name is typed as string (not ?string) in this class. + * + * @return string */ - public function getName(): ?string + public function getName(): string { return $this->name; } @@ -234,7 +237,7 @@ public function setNull(bool $null) */ public function getNull(): bool { - return $this->null; + return $this->null ?? false; } /** @@ -782,10 +785,10 @@ public function getLock(): ?string * * When true, binary columns will use BINARY(n) instead of VARBINARY(n). * - * @param bool $fixed Fixed + * @param bool|null $fixed Fixed * @return $this */ - public function setFixed(bool $fixed) + public function setFixed(?bool $fixed) { $this->fixed = $fixed; diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php index 907e5f375..425b9f68a 100644 --- a/src/Db/Table/ForeignKey.php +++ b/src/Db/Table/ForeignKey.php @@ -86,6 +86,19 @@ public function __construct( } } + /** + * {@inheritDoc} + * + * Narrows the return type from the parent's ?array to array, + * since $columns is always initialized as [] in this class. + * + * @return array + */ + public function getColumns(): array + { + return $this->columns; + } + /** * Utility method that maps an array of index options to this object's methods. * @@ -246,7 +259,9 @@ public function setOnDelete(string $onDelete) */ public function getOnDelete(): ?string { - return $this->mapAction($this->getDelete()); + $delete = $this->getDelete(); + + return $delete !== null ? $this->mapAction($delete) : null; } /** @@ -271,6 +286,8 @@ public function setOnUpdate(string $onUpdate) */ public function getOnUpdate(): ?string { - return $this->mapAction($this->getUpdate()); + $update = $this->getUpdate(); + + return $update !== null ? $this->mapAction($update) : null; } } diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php index 97b9efdc0..e16d1ab83 100644 --- a/src/Util/ColumnParser.php +++ b/src/Util/ColumnParser.php @@ -213,7 +213,7 @@ public function parseForeignKeys(array $arguments): array $referencedTable = $indexType; if (!$referencedTable) { // Remove common suffixes like _id and pluralize - $referencedTable = preg_replace('/_id$/', '', $fieldName); + $referencedTable = (string)preg_replace('/_id$/', '', $fieldName); $referencedTable = Inflector::pluralize($referencedTable); } diff --git a/src/Util/TableFinder.php b/src/Util/TableFinder.php index a96b4e03b..5e75c8d68 100644 --- a/src/Util/TableFinder.php +++ b/src/Util/TableFinder.php @@ -183,9 +183,10 @@ public function fetchTableName(string $className, ?string $pluginName = null): a $table = TableRegistry::getTableLocator()->get($className); foreach ($table->associations()->keys() as $key) { - if ($table->associations()->get($key)->type() === 'belongsToMany') { + $association = $table->associations()->get($key); + if ($association !== null && $association->type() === 'belongsToMany') { /** @var \Cake\ORM\Association\BelongsToMany $belongsToMany */ - $belongsToMany = $table->associations()->get($key); + $belongsToMany = $association; $tables[] = $belongsToMany->junction()->getTable(); } } diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 75e04cbb0..c933c6275 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -693,7 +693,7 @@ public function getCreateTableData(TableSchemaInterface|string $table): array $indexes = $this->indexes($table); $foreignKeys = []; foreach ($constraints as $constraint) { - if ($constraint['type'] === 'foreign') { + if (isset($constraint['type']) && $constraint['type'] === 'foreign') { $foreignKeys[] = $constraint['columns']; } } diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 2d5ccd1ae..41af49f73 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -308,6 +308,17 @@ public function testSetOptionsWithFixed(): void $this->assertTrue($column->getFixed()); $this->assertSame(20, $column->getLimit()); + + // Null via setter should be accepted and preserved + $column2 = new Column(); + $column2->setName('data')->setType('binary')->setFixed(null); + $this->assertNull($column2->getFixed()); + + // Null via setOptions (as generated by migration snapshots) + $column3 = new Column(); + $column3->setName('data')->setType('binary'); + $column3->setOptions(['fixed' => null, 'limit' => null, 'null' => false]); + $this->assertNull($column3->getFixed()); } public function testToArrayIncludesFixed(): void diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 21912fd58..1be0f352a 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -80,6 +80,15 @@ public function testAddColumnWithColumnObject() $this->assertSame($column, $actions[0]->getColumn()); } + public function testAddColumnWithNullTypeThrows() + { + $adapter = new MysqlAdapter([]); + $table = new Table('ntable', [], $adapter); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Column type must not be null when column name is a string.'); + $table->addColumn('email', null); + } + public function testAddColumnWithNoAdapterSpecified() { try { From 273b420bef66e716d04d734927040713acc8d72c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 11 Mar 2026 21:13:36 +0100 Subject: [PATCH 13/72] 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 907e5f375..3e4a4d394 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 774cf80d8..9f67c0e5c 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 e1370cd4f..e36e7fb44 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 21912fd58..463e9aa86 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 a889d9e7c..4dce76e41 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 54b61d85f..52e1f0766 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 93edeb6d8..65565a196 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 eca100cc3..bbe494204 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 e297c18ce..8ff44ed60 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 2541ce034..25f27019b 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 1191bd8c9..2c3cb4f59 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 24da5e35f..12261cdd3 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 e292c18e3..bef6d8cea 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 78ae984e4..f9f840281 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 f1fbd6eec..857bbde1d 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 347b5cdd5..47b630bfd 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 e0aad74e8..aa2bc9a34 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 c8d0e8dbf..8e49310d4 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 36ef3a9b1..f1282faf4 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 b50ad2f92..b6c5ccfb2 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 11c01ca2b..5537e448e 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 a4829f3c2..6897dd2aa 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 9024c29ad..c8d07b215 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 1a277d711..4bde415d7 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 20dd3fba9..d33673594 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 51b0002a4..e4f7da4f7 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 df2ea8ba0..3da752b24 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 11fadb44f..13f8327da 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 93570e1ec..bdf099943 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 ec5f12a64..d4a432337 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 45cad82a0..c48d00a4e 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 5abc57314e6fc66f75c7f700062eae82dd8c067d Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 12 Mar 2026 11:05:54 +0100 Subject: [PATCH 14/72] Add support for database views and triggers (#957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for database views and triggers (#347) This implements support for creating and managing database views and triggers through CakePHP migrations, addressing issue #347. - Create and drop database views in migrations - Support for OR REPLACE syntax (MySQL, PostgreSQL) - Materialized views support (PostgreSQL only) - Database-agnostic API with adapter-specific implementations - Create and drop database triggers in migrations - Support for BEFORE/AFTER/INSTEAD OF timing - Support for INSERT/UPDATE/DELETE events - Support for multiple events per trigger - FOR EACH ROW vs FOR EACH STATEMENT options **Value Objects:** - `Migrations\Db\Table\View` - Represents a database view - `Migrations\Db\Table\Trigger` - Represents a database trigger **Actions:** - `Migrations\Db\Action\CreateView` - Action for creating views - `Migrations\Db\Action\DropView` - Action for dropping views - `Migrations\Db\Action\CreateTrigger` - Action for creating triggers - `Migrations\Db\Action\DropTrigger` - Action for dropping triggers **Core:** - `AbstractAdapter` - Added abstract methods for view/trigger support - `Table` - Added createView(), dropView(), createTrigger(), dropTrigger() - `BaseMigration` - Added convenience methods for easy migration usage **Adapters:** - `MysqlAdapter` - MySQL-specific view/trigger SQL generation - `PostgresAdapter` - PostgreSQL implementation with materialized views - `SqliteAdapter` - SQLite-specific syntax handling - `SqlserverAdapter` - SQL Server implementation ```php // Create a view $this->createView( 'active_users', 'SELECT * FROM users WHERE status = "active"' ); // Create a materialized view (PostgreSQL) $this->createView( 'user_stats', 'SELECT user_id, COUNT(*) FROM posts GROUP BY user_id', ['materialized' => true] ); // Create a trigger $this->createTrigger( 'users', 'log_changes', 'INSERT', "INSERT INTO audit_log VALUES (NEW.id, NOW())", ['timing' => 'AFTER'] ); // Drop view/trigger $this->dropView('active_users'); $this->dropTrigger('users', 'log_changes'); ``` Added comprehensive tests for view and trigger functionality in MysqlAdapterTest covering creation, querying, and deletion. Added example migration file demonstrating various view and trigger scenarios with database-specific considerations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude * Fix AbstractAdapterTest by adding stub implementations for view/trigger methods The anonymous test class extending AbstractAdapter needs to implement the new abstract methods for view and trigger support. * Fixes. * Fixes. * Fix missing imports and add return types after merge - Add missing `use Exception` and `use Migrations\Db\Table` imports to AbstractAdapter - Add `: void` return types to algorithm/lock test methods in MysqlAdapterTest - Fix alphabetical ordering of use statements --- docs/examples/ViewsAndTriggersExample.php | 149 ++++++++++++++ src/BaseMigration.php | 60 ++++++ src/Db/Action/CreateTrigger.php | 38 ++++ src/Db/Action/CreateView.php | 38 ++++ src/Db/Action/DropTrigger.php | 37 ++++ src/Db/Action/DropView.php | 49 +++++ src/Db/Adapter/AbstractAdapter.php | 70 +++++++ src/Db/Adapter/MysqlAdapter.php | 63 ++++++ src/Db/Adapter/PostgresAdapter.php | 77 ++++++++ src/Db/Adapter/SqliteAdapter.php | 65 +++++++ src/Db/Adapter/SqlserverAdapter.php | 74 +++++++ src/Db/Plan/Plan.php | 42 ++++ src/Db/Table.php | 109 ++++++++++- src/Db/Table/Trigger.php | 183 ++++++++++++++++++ src/Db/Table/View.php | 128 ++++++++++++ .../Db/Adapter/DefaultAdapterTrait.php | 22 +++ .../TestCase/Db/Adapter/MysqlAdapterTest.php | 128 ++++++++++-- 17 files changed, 1318 insertions(+), 14 deletions(-) create mode 100644 docs/examples/ViewsAndTriggersExample.php create mode 100644 src/Db/Action/CreateTrigger.php create mode 100644 src/Db/Action/CreateView.php create mode 100644 src/Db/Action/DropTrigger.php create mode 100644 src/Db/Action/DropView.php create mode 100644 src/Db/Table/Trigger.php create mode 100644 src/Db/Table/View.php diff --git a/docs/examples/ViewsAndTriggersExample.php b/docs/examples/ViewsAndTriggersExample.php new file mode 100644 index 000000000..1c381735c --- /dev/null +++ b/docs/examples/ViewsAndTriggersExample.php @@ -0,0 +1,149 @@ +table('users'); + $users->addColumn('username', 'string', ['limit' => 100]) + ->addColumn('email', 'string', ['limit' => 255]) + ->addColumn('status', 'string', ['limit' => 20, 'default' => 'active']) + ->addColumn('created', 'datetime') + ->create(); + + // Create a posts table + $posts = $this->table('posts'); + $posts->addColumn('user_id', 'integer') + ->addColumn('title', 'string', ['limit' => 255]) + ->addColumn('body', 'text') + ->addColumn('published', 'boolean', ['default' => false]) + ->addColumn('created', 'datetime') + ->addForeignKey('user_id', 'users', 'id', ['delete' => 'CASCADE']) + ->create(); + + // Create a view showing active users with their post counts + // Note: Views are created through a dummy table object + $this->createView( + 'active_users_with_posts', + 'SELECT u.id, u.username, u.email, COUNT(p.id) as post_count + FROM users u + LEFT JOIN posts p ON u.id = p.user_id + WHERE u.status = \'active\' + GROUP BY u.id, u.username, u.email' + ); + + // Create a materialized view (PostgreSQL only) + // On other databases, this will create a regular view + $this->createView( + 'published_posts_summary', + 'SELECT user_id, COUNT(*) as published_count + FROM posts + WHERE published = 1 + GROUP BY user_id', + ['materialized' => true] + ); + + // Create an audit log table for triggers + $auditLog = $this->table('audit_log'); + $auditLog->addColumn('table_name', 'string', ['limit' => 100]) + ->addColumn('action', 'string', ['limit' => 20]) + ->addColumn('record_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + + // Create a trigger to log user insertions + // Note: The trigger definition syntax varies by database + + // For MySQL: + $this->createTrigger( + 'users', + 'log_user_insert', + 'INSERT', + "INSERT INTO audit_log (table_name, action, record_id, created) + VALUES ('users', 'INSERT', NEW.id, NOW())", + ['timing' => 'AFTER'] + ); + + // For PostgreSQL, you would need to create a function first: + // $this->execute(" + // CREATE OR REPLACE FUNCTION log_user_insert_func() + // RETURNS TRIGGER AS $$ + // BEGIN + // INSERT INTO audit_log (table_name, action, record_id, created) + // VALUES ('users', 'INSERT', NEW.id, NOW()); + // RETURN NEW; + // END; + // $$ LANGUAGE plpgsql; + // "); + // + // $this->createTrigger( + // 'users', + // 'log_user_insert', + // 'INSERT', + // 'log_user_insert_func()', // Function name for PostgreSQL + // ['timing' => 'AFTER'] + // ); + + // Create a trigger for updates with multiple events + $this->createTrigger( + 'posts', + 'log_post_changes', + ['UPDATE', 'DELETE'], + "INSERT INTO audit_log (table_name, action, record_id, created) + VALUES ('posts', 'CHANGE', OLD.id, NOW())", + ['timing' => 'BEFORE'] + ); + } + + /** + * Migrate Up. + * + * If you need more control, you can use up() and down() methods instead. + */ + public function up(): void + { + // Example of creating a view in up() method + $this->createView( + 'simple_user_list', + 'SELECT id, username FROM users' + ); + } + + /** + * Migrate Down. + */ + public function down(): void + { + // Drop views + $this->dropView('simple_user_list'); + $this->dropView('active_users_with_posts'); + $this->dropView('published_posts_summary', ['materialized' => true]); + + // Drop triggers + $this->dropTrigger('users', 'log_user_insert'); + $this->dropTrigger('posts', 'log_post_changes'); + + // Drop tables + $this->table('audit_log')->drop()->save(); + $this->table('posts')->drop()->save(); + $this->table('users')->drop()->save(); + } +} diff --git a/src/BaseMigration.php b/src/BaseMigration.php index 2969933aa..910700c0a 100644 --- a/src/BaseMigration.php +++ b/src/BaseMigration.php @@ -498,6 +498,66 @@ public function shouldExecute(): bool return true; } + /** + * Creates a view. + * + * This is a convenience method that creates a dummy table to associate the view with. + * Views are not directly associated with tables, but the Table class is used to + * manage the migration actions. + * + * @param string $viewName View name + * @param string $definition SQL SELECT statement for the view + * @param array $options View options + * @return void + */ + public function createView(string $viewName, string $definition, array $options = []): void + { + $table = $this->table($viewName); + $table->createView($viewName, $definition, $options)->save(); + } + + /** + * Drops a view. + * + * @param string $viewName View name + * @param array $options View options + * @return void + */ + public function dropView(string $viewName, array $options = []): void + { + $table = $this->table($viewName); + $table->dropView($viewName, $options)->save(); + } + + /** + * Creates a trigger on a table. + * + * @param string $tableName Table name + * @param string $triggerName Trigger name + * @param string|array $event Event(s) that fire the trigger (INSERT, UPDATE, DELETE) + * @param string $definition Trigger body/definition + * @param array $options Trigger options + * @return void + */ + public function createTrigger(string $tableName, string $triggerName, string|array $event, string $definition, array $options = []): void + { + $table = $this->table($tableName); + $table->createTrigger($triggerName, $event, $definition, $options)->save(); + } + + /** + * Drops a trigger from a table. + * + * @param string $tableName Table name + * @param string $triggerName Trigger name + * @return void + */ + public function dropTrigger(string $tableName, string $triggerName): void + { + $table = $this->table($tableName); + $table->dropTrigger($triggerName)->save(); + } + /** * Makes sure the version int is within range for valid datetime. * This is required to have a meaningful order in the overview. diff --git a/src/Db/Action/CreateTrigger.php b/src/Db/Action/CreateTrigger.php new file mode 100644 index 000000000..fdb592f84 --- /dev/null +++ b/src/Db/Action/CreateTrigger.php @@ -0,0 +1,38 @@ +trigger; + } +} diff --git a/src/Db/Action/CreateView.php b/src/Db/Action/CreateView.php new file mode 100644 index 000000000..51ed0d5d4 --- /dev/null +++ b/src/Db/Action/CreateView.php @@ -0,0 +1,38 @@ +view; + } +} diff --git a/src/Db/Action/DropTrigger.php b/src/Db/Action/DropTrigger.php new file mode 100644 index 000000000..2077c9dd1 --- /dev/null +++ b/src/Db/Action/DropTrigger.php @@ -0,0 +1,37 @@ +triggerName; + } +} diff --git a/src/Db/Action/DropView.php b/src/Db/Action/DropView.php new file mode 100644 index 000000000..eba4324e2 --- /dev/null +++ b/src/Db/Action/DropView.php @@ -0,0 +1,49 @@ +viewName; + } + + /** + * Gets whether this is a materialized view + * + * @return bool + */ + public function getMaterialized(): bool + { + return $this->materialized; + } +} diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 367e89cc4..01497ea1c 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -30,10 +30,14 @@ use Migrations\Db\Action\ChangeColumn; use Migrations\Db\Action\ChangeComment; use Migrations\Db\Action\ChangePrimaryKey; +use Migrations\Db\Action\CreateTrigger; +use Migrations\Db\Action\CreateView; use Migrations\Db\Action\DropForeignKey; use Migrations\Db\Action\DropIndex; use Migrations\Db\Action\DropPartition; use Migrations\Db\Action\DropTable; +use Migrations\Db\Action\DropTrigger; +use Migrations\Db\Action\DropView; use Migrations\Db\Action\RemoveColumn; use Migrations\Db\Action\RenameColumn; use Migrations\Db\Action\RenameTable; @@ -48,6 +52,8 @@ use Migrations\Db\Table\Index; use Migrations\Db\Table\Partition; use Migrations\Db\Table\TableMetadata; +use Migrations\Db\Table\Trigger; +use Migrations\Db\Table\View; use Migrations\MigrationInterface; use Migrations\SeedInterface; use PDOException; @@ -1638,6 +1644,41 @@ public function changeComment(TableMetadata $table, $newComment): void */ abstract protected function getChangeCommentInstructions(TableMetadata $table, ?string $newComment): AlterInstructions; + /** + * Returns the instructions to create a view. + * + * @param \Migrations\Db\Table\View $view The view to create + * @return \Migrations\Db\AlterInstructions + */ + abstract protected function getCreateViewInstructions(View $view): AlterInstructions; + + /** + * Returns the instructions to drop a view. + * + * @param string $viewName The name of the view to drop + * @param bool $materialized Whether this is a materialized view (PostgreSQL only) + * @return \Migrations\Db\AlterInstructions + */ + abstract protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions; + + /** + * Returns the instructions to create a trigger. + * + * @param string $tableName The name of the table for the trigger + * @param \Migrations\Db\Table\Trigger $trigger The trigger to create + * @return \Migrations\Db\AlterInstructions + */ + abstract protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions; + + /** + * Returns the instructions to drop a trigger. + * + * @param string $tableName The name of the table for the trigger + * @param string $triggerName The name of the trigger to drop + * @return \Migrations\Db\AlterInstructions + */ + abstract protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions; + /** * {@inheritDoc} * @@ -1780,6 +1821,35 @@ public function executeActions(TableMetadata $table, array $actions): void )); break; + case $action instanceof CreateView: + /** @var \Migrations\Db\Action\CreateView $action */ + $instructions->merge($this->getCreateViewInstructions($action->getView())); + break; + + case $action instanceof DropView: + /** @var \Migrations\Db\Action\DropView $action */ + $instructions->merge($this->getDropViewInstructions( + $action->getViewName(), + $action->getMaterialized(), + )); + break; + + case $action instanceof CreateTrigger: + /** @var \Migrations\Db\Action\CreateTrigger $action */ + $instructions->merge($this->getCreateTriggerInstructions( + $table->getName(), + $action->getTrigger(), + )); + break; + + case $action instanceof DropTrigger: + /** @var \Migrations\Db\Action\DropTrigger $action */ + $instructions->merge($this->getDropTriggerInstructions( + $table->getName(), + $action->getTriggerName(), + )); + break; + default: throw new InvalidArgumentException( sprintf("Don't know how to execute action `%s`", get_class($action)), diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 6dd6c0ba6..ce3fbe4a1 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -23,6 +23,8 @@ use Migrations\Db\Table\Partition; use Migrations\Db\Table\PartitionDefinition; use Migrations\Db\Table\TableMetadata; +use Migrations\Db\Table\Trigger; +use Migrations\Db\Table\View; /** * MySQL Adapter. @@ -1587,4 +1589,65 @@ protected function executeAlterSteps(string $tableName, AlterInstructions $instr $this->execute($instruction); } } + + /** + * {@inheritDoc} + */ + protected function getCreateViewInstructions(View $view): AlterInstructions + { + $sql = sprintf( + 'CREATE %sVIEW %s AS %s', + $view->getReplace() ? 'OR REPLACE ' : '', + $this->quoteTableName($view->getName()), + $view->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions + { + $sql = sprintf( + 'DROP VIEW IF EXISTS %s', + $this->quoteTableName($viewName), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions + { + $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()]; + $eventStr = implode(' OR ', $events); + + $sql = sprintf( + 'CREATE TRIGGER %s %s %s ON %s FOR EACH ROW %s', + $this->quoteColumnName($trigger->getName()), + $trigger->getTiming(), + $eventStr, + $this->quoteTableName($tableName), + $trigger->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions + { + $sql = sprintf( + 'DROP TRIGGER IF EXISTS %s', + $this->quoteColumnName($triggerName), + ); + + return new AlterInstructions([], [$sql]); + } } diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 899c1ee92..851272989 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -23,6 +23,8 @@ use Migrations\Db\Table\Partition; use Migrations\Db\Table\PartitionDefinition; use Migrations\Db\Table\TableMetadata; +use Migrations\Db\Table\Trigger; +use Migrations\Db\Table\View; use RuntimeException; class PostgresAdapter extends AbstractAdapter @@ -1579,4 +1581,79 @@ public function getAdapterType(): string // compatibility. return 'pgsql'; } + + /** + * {@inheritDoc} + */ + protected function getCreateViewInstructions(View $view): AlterInstructions + { + $viewType = $view->getMaterialized() ? 'MATERIALIZED VIEW' : 'VIEW'; + $replace = ''; + + if ($view->getReplace() && !$view->getMaterialized()) { + $replace = 'OR REPLACE '; + } + + $sql = sprintf( + 'CREATE %s%s %s AS %s', + $replace, + $viewType, + $this->quoteTableName($view->getName()), + $view->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions + { + $viewType = $materialized ? 'MATERIALIZED VIEW' : 'VIEW'; + $sql = sprintf( + 'DROP %s IF EXISTS %s', + $viewType, + $this->quoteTableName($viewName), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions + { + $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()]; + $eventStr = implode(' OR ', $events); + + $forEach = $trigger->getForEach() ? 'FOR EACH ROW' : 'FOR EACH STATEMENT'; + + $sql = sprintf( + 'CREATE TRIGGER %s %s %s ON %s %s EXECUTE FUNCTION %s', + $this->quoteColumnName($trigger->getName()), + $trigger->getTiming(), + $eventStr, + $this->quoteTableName($tableName), + $forEach, + $trigger->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions + { + $sql = sprintf( + 'DROP TRIGGER IF EXISTS %s ON %s', + $this->quoteColumnName($triggerName), + $this->quoteTableName($tableName), + ); + + return new AlterInstructions([], [$sql]); + } } diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 2cd6894da..8a157e156 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -21,6 +21,8 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\TableMetadata; +use Migrations\Db\Table\Trigger; +use Migrations\Db\Table\View; use PDOException; use RuntimeException; use const FILTER_VALIDATE_BOOLEAN; @@ -1751,4 +1753,67 @@ protected function getUpsertClause(?InsertMode $mode, ?array $updateColumns, ?ar return ' ON CONFLICT (' . implode(', ', $quotedConflictColumns) . ') DO UPDATE SET ' . implode(', ', $updates); } + + /** + * {@inheritDoc} + */ + protected function getCreateViewInstructions(View $view): AlterInstructions + { + $sql = sprintf( + 'CREATE VIEW IF NOT EXISTS %s AS %s', + $this->quoteTableName($view->getName()), + $view->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions + { + $sql = sprintf( + 'DROP VIEW IF EXISTS %s', + $this->quoteTableName($viewName), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions + { + $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()]; + $eventStr = implode(' OR ', $events); + + $forEach = $trigger->getForEach() ? 'FOR EACH ROW' : ''; + + $sql = sprintf( + 'CREATE TRIGGER %s %s %s ON %s %s BEGIN %s END', + $this->quoteColumnName($trigger->getName()), + $trigger->getTiming(), + $eventStr, + $this->quoteTableName($tableName), + $forEach, + $trigger->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions + { + $sql = sprintf( + 'DROP TRIGGER IF EXISTS %s', + $this->quoteColumnName($triggerName), + ); + + return new AlterInstructions([], [$sql]); + } } diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index 43ede29ca..b9a47e1e9 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -21,6 +21,8 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\TableMetadata; +use Migrations\Db\Table\Trigger; +use Migrations\Db\Table\View; use Migrations\MigrationInterface; /** @@ -1171,4 +1173,76 @@ protected function getInsertPrefix(?InsertMode $mode = null): string return parent::getInsertPrefix($mode); } + + /** + * {@inheritDoc} + */ + protected function getCreateViewInstructions(View $view): AlterInstructions + { + $drop = ''; + if ($view->getReplace()) { + $drop = sprintf( + "IF OBJECT_ID('%s', 'V') IS NOT NULL DROP VIEW %s; ", + $view->getName(), + $this->quoteTableName($view->getName()), + ); + } + + $sql = sprintf( + '%sCREATE VIEW %s AS %s', + $drop, + $this->quoteTableName($view->getName()), + $view->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions + { + $sql = sprintf( + "IF OBJECT_ID('%s', 'V') IS NOT NULL DROP VIEW %s", + $viewName, + $this->quoteTableName($viewName), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions + { + $events = is_array($trigger->getEvent()) ? $trigger->getEvent() : [$trigger->getEvent()]; + $eventStr = implode(', ', $events); + + $sql = sprintf( + 'CREATE TRIGGER %s ON %s %s %s AS %s', + $this->quoteColumnName($trigger->getName()), + $this->quoteTableName($tableName), + $trigger->getTiming(), + $eventStr, + $trigger->getDefinition(), + ); + + return new AlterInstructions([], [$sql]); + } + + /** + * {@inheritDoc} + */ + protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions + { + $sql = sprintf( + "IF OBJECT_ID('%s', 'TR') IS NOT NULL DROP TRIGGER %s", + $triggerName, + $this->quoteColumnName($triggerName), + ); + + return new AlterInstructions([], [$sql]); + } } diff --git a/src/Db/Plan/Plan.php b/src/Db/Plan/Plan.php index dcbaa718d..6c4930332 100644 --- a/src/Db/Plan/Plan.php +++ b/src/Db/Plan/Plan.php @@ -17,10 +17,14 @@ use Migrations\Db\Action\ChangeComment; use Migrations\Db\Action\ChangePrimaryKey; use Migrations\Db\Action\CreateTable; +use Migrations\Db\Action\CreateTrigger; +use Migrations\Db\Action\CreateView; use Migrations\Db\Action\DropForeignKey; use Migrations\Db\Action\DropIndex; use Migrations\Db\Action\DropPartition; use Migrations\Db\Action\DropTable; +use Migrations\Db\Action\DropTrigger; +use Migrations\Db\Action\DropView; use Migrations\Db\Action\RemoveColumn; use Migrations\Db\Action\RenameColumn; use Migrations\Db\Action\RenameTable; @@ -87,6 +91,13 @@ class Plan */ protected array $columnRemoves = []; + /** + * List of view and trigger operations + * + * @var \Migrations\Db\Plan\AlterTable[] + */ + protected array $viewsAndTriggers = []; + /** * Constructor * @@ -111,6 +122,7 @@ protected function createPlan(array $actions): void $this->gatherIndexes($actions); $this->gatherConstraints($actions); $this->gatherPartitions($actions); + $this->gatherViewsAndTriggers($actions); $this->resolveConflicts(); } @@ -126,6 +138,7 @@ protected function updatesSequence(): array $this->constraints, $this->indexes, $this->partitions, + $this->viewsAndTriggers, $this->columnRemoves, $this->tableMoves, ]; @@ -141,6 +154,7 @@ protected function inverseUpdatesSequence(): array return [ $this->constraints, $this->tableMoves, + $this->viewsAndTriggers, $this->partitions, $this->indexes, $this->columnRemoves, @@ -534,4 +548,32 @@ protected function gatherPartitions(array $actions): void $this->partitions[$name]->addAction($action); } } + + /** + * Collects all view and trigger creation and drops from the given intent + * + * @param \Migrations\Db\Action\Action[] $actions The actions to parse + * @return void + */ + protected function gatherViewsAndTriggers(array $actions): void + { + foreach ($actions as $action) { + if ( + !($action instanceof CreateView) + && !($action instanceof DropView) + && !($action instanceof CreateTrigger) + && !($action instanceof DropTrigger) + ) { + continue; + } + $table = $action->getTable(); + $name = $table->getName(); + + if (!isset($this->viewsAndTriggers[$name])) { + $this->viewsAndTriggers[$name] = new AlterTable($table); + } + + $this->viewsAndTriggers[$name]->addAction($action); + } + } } diff --git a/src/Db/Table.php b/src/Db/Table.php index c83d7fd5f..6ba2c036c 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -19,10 +19,14 @@ use Migrations\Db\Action\ChangeComment; use Migrations\Db\Action\ChangePrimaryKey; use Migrations\Db\Action\CreateTable; +use Migrations\Db\Action\CreateTrigger; +use Migrations\Db\Action\CreateView; use Migrations\Db\Action\DropForeignKey; use Migrations\Db\Action\DropIndex; use Migrations\Db\Action\DropPartition; use Migrations\Db\Action\DropTable; +use Migrations\Db\Action\DropTrigger; +use Migrations\Db\Action\DropView; use Migrations\Db\Action\RemoveColumn; use Migrations\Db\Action\RenameColumn; use Migrations\Db\Action\RenameTable; @@ -1015,6 +1019,87 @@ public function save(): void } } + /** + * Creates a view. + * + * @param string $viewName View name + * @param string $definition SQL SELECT statement for the view + * @param array $options View options + * @return $this + */ + public function createView(string $viewName, string $definition, array $options = []) + { + $view = new Table\View( + $viewName, + $definition, + $options['replace'] ?? false, + $options['materialized'] ?? false, + ); + + $action = new Action\CreateView($this->table, $view); + $this->actions->addAction($action); + + return $this; + } + + /** + * Drops a view. + * + * @param string $viewName View name + * @param array $options View options + * @return $this + */ + public function dropView(string $viewName, array $options = []) + { + $action = new Action\DropView( + $this->table, + $viewName, + $options['materialized'] ?? false, + ); + $this->actions->addAction($action); + + return $this; + } + + /** + * Creates a trigger on this table. + * + * @param string $triggerName Trigger name + * @param string|array $event Event(s) that fire the trigger (INSERT, UPDATE, DELETE) + * @param string $definition Trigger body/definition + * @param array $options Trigger options + * @return $this + */ + public function createTrigger(string $triggerName, string|array $event, string $definition, array $options = []) + { + $trigger = new Table\Trigger( + $triggerName, + $options['timing'] ?? Table\Trigger::BEFORE, + $event, + $definition, + $options['forEach'] ?? true, + ); + + $action = new Action\CreateTrigger($this->table, $trigger); + $this->actions->addAction($action); + + return $this; + } + + /** + * Drops a trigger from this table. + * + * @param string $triggerName Trigger name + * @return $this + */ + public function dropTrigger(string $triggerName) + { + $action = new Action\DropTrigger($this->table, $triggerName); + $this->actions->addAction($action); + + return $this; + } + /** * Executes all the pending actions for this table * @@ -1044,9 +1129,29 @@ protected function executeActions(bool $exists): void } // If the table does not exist, the last command in the chain needs to be - // a CreateTable action. + // a CreateTable action - unless we're ONLY creating views/triggers. if (!$exists) { - $this->actions->addAction(new CreateTable($this->table)); + $actions = $this->actions->getActions(); + $hasTableActions = false; + $hasViewOrTriggerActions = false; + + foreach ($actions as $action) { + if ( + $action instanceof CreateView + || $action instanceof DropView + || $action instanceof CreateTrigger + || $action instanceof DropTrigger + ) { + $hasViewOrTriggerActions = true; + } else { + $hasTableActions = true; + } + } + + // Only skip CreateTable if we have ONLY view/trigger actions (and at least one) + if (!$hasViewOrTriggerActions || $hasTableActions || count($actions) === 0) { + $this->actions->addAction(new CreateTable($this->table)); + } } $plan = new Plan($this->actions); diff --git a/src/Db/Table/Trigger.php b/src/Db/Table/Trigger.php new file mode 100644 index 000000000..a4e6286cd --- /dev/null +++ b/src/Db/Table/Trigger.php @@ -0,0 +1,183 @@ + $event The event(s) that fire the trigger (INSERT, UPDATE, DELETE). + * @param string $definition The trigger body/definition. + * @param bool $forEach Whether to fire for each row (true) or statement (false). + */ + public function __construct( + protected string $name = '', + protected string $timing = self::BEFORE, + protected string|array $event = self::INSERT, + protected string $definition = '', + protected bool $forEach = true, + ) { + } + + /** + * Sets the trigger name. + * + * @param string $name Name + * @return $this + */ + public function setName(string $name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the trigger name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Sets the trigger timing (BEFORE, AFTER, or INSTEAD OF). + * + * @param string $timing Timing + * @return $this + */ + public function setTiming(string $timing) + { + $this->timing = $timing; + + return $this; + } + + /** + * Gets the trigger timing. + * + * @return string + */ + public function getTiming(): string + { + return $this->timing; + } + + /** + * Sets the trigger event(s). + * + * @param string|array $event Event(s) + * @return $this + */ + public function setEvent(string|array $event) + { + $this->event = $event; + + return $this; + } + + /** + * Gets the trigger event(s). + * + * @return string|array + */ + public function getEvent(): string|array + { + return $this->event; + } + + /** + * Sets the trigger definition/body. + * + * @param string $definition Definition + * @return $this + */ + public function setDefinition(string $definition) + { + $this->definition = $definition; + + return $this; + } + + /** + * Gets the trigger definition. + * + * @return string + */ + public function getDefinition(): string + { + return $this->definition; + } + + /** + * Sets whether to fire for each row (true) or per statement (false). + * + * @param bool $forEach For each row flag + * @return $this + */ + public function setForEach(bool $forEach) + { + $this->forEach = $forEach; + + return $this; + } + + /** + * Gets whether to fire for each row. + * + * @return bool + */ + public function getForEach(): bool + { + return $this->forEach; + } +} diff --git a/src/Db/Table/View.php b/src/Db/Table/View.php new file mode 100644 index 000000000..ac45da392 --- /dev/null +++ b/src/Db/Table/View.php @@ -0,0 +1,128 @@ +name = $name; + + return $this; + } + + /** + * Gets the view name. + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Sets the view definition (SELECT statement). + * + * @param string $definition Definition + * @return $this + */ + public function setDefinition(string $definition) + { + $this->definition = $definition; + + return $this; + } + + /** + * Gets the view definition. + * + * @return string + */ + public function getDefinition(): string + { + return $this->definition; + } + + /** + * Sets whether to replace the view if it exists. + * + * @param bool $replace Replace flag + * @return $this + */ + public function setReplace(bool $replace) + { + $this->replace = $replace; + + return $this; + } + + /** + * Gets whether to replace the view if it exists. + * + * @return bool + */ + public function getReplace(): bool + { + return $this->replace; + } + + /** + * Sets whether this is a materialized view (PostgreSQL only). + * + * @param bool $materialized Materialized flag + * @return $this + */ + public function setMaterialized(bool $materialized) + { + $this->materialized = $materialized; + + return $this; + } + + /** + * Gets whether this is a materialized view. + * + * @return bool + */ + public function getMaterialized(): bool + { + return $this->materialized; + } +} diff --git a/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php b/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php index 61543f28b..e92a59656 100644 --- a/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php +++ b/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php @@ -9,6 +9,8 @@ use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; use Migrations\Db\Table\TableMetadata; +use Migrations\Db\Table\Trigger; +use Migrations\Db\Table\View; trait DefaultAdapterTrait { @@ -187,4 +189,24 @@ protected function getDropCheckConstraintInstructions(string $tableName, string { return new AlterInstructions(); } + + protected function getCreateViewInstructions(View $view): AlterInstructions + { + return new AlterInstructions(); + } + + protected function getDropViewInstructions(string $viewName, bool $materialized = false): AlterInstructions + { + return new AlterInstructions(); + } + + protected function getCreateTriggerInstructions(string $tableName, Trigger $trigger): AlterInstructions + { + return new AlterInstructions(); + } + + protected function getDropTriggerInstructions(string $tableName, string $triggerName): AlterInstructions + { + return new AlterInstructions(); + } } diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 1ab36b5c8..c08f818fa 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -2843,7 +2843,111 @@ public function testInsertOrSkipWithoutDuplicates() $this->assertCount(2, $rows); } - public function testAddColumnWithAlgorithmInstant() + public function testCreateView(): void + { + // Create a base table + $table = new Table('users', [], $this->adapter); + $table->addColumn('name', 'string') + ->addColumn('email', 'string') + ->create(); + + // Insert some data + $table->insert([ + ['name' => 'Alice', 'email' => 'alice@example.com'], + ['name' => 'Bob', 'email' => 'bob@example.com'], + ])->save(); + + // Create a view + $viewTable = new Table('user_emails', [], $this->adapter); + $viewTable->createView('user_emails', 'SELECT name, email FROM users') + ->save(); + + // Query the view + $rows = $this->adapter->fetchAll('SELECT * FROM user_emails'); + $this->assertCount(2, $rows); + $this->assertEquals('Alice', $rows[0]['name']); + $this->assertEquals('alice@example.com', $rows[0]['email']); + } + + public function testDropView(): void + { + // Create a base table + $table = new Table('users', [], $this->adapter); + $table->addColumn('name', 'string')->create(); + + // Create a view + $viewTable = new Table('user_names', [], $this->adapter); + $viewTable->createView('user_names', 'SELECT name FROM users')->save(); + + // Verify view exists + $rows = $this->adapter->fetchAll('SELECT * FROM user_names'); + $this->assertIsArray($rows); + + // Drop the view + $viewTable->dropView('user_names')->save(); + + // Verify view is dropped + $this->expectException(PDOException::class); + $this->adapter->fetchAll('SELECT * FROM user_names'); + } + + public function testCreateTrigger(): void + { + // Create tables + $table = new Table('users', [], $this->adapter); + $table->addColumn('name', 'string') + ->addColumn('created_count', 'integer', ['default' => 0]) + ->create(); + + $logTable = new Table('user_log', [], $this->adapter); + $logTable->addColumn('action', 'string')->create(); + + // Create a trigger + $table->createTrigger( + 'log_user_insert', + 'INSERT', + "INSERT INTO user_log (action) VALUES ('user_created')", + ['timing' => 'AFTER'], + )->save(); + + // Insert data to trigger the trigger + $table->insert(['name' => 'Alice', 'created_count' => 0])->save(); + + // Verify trigger fired + $rows = $this->adapter->fetchAll('SELECT * FROM user_log'); + $this->assertCount(1, $rows); + $this->assertEquals('user_created', $rows[0]['action']); + } + + public function testDropTrigger(): void + { + // Create table + $table = new Table('users', [], $this->adapter); + $table->addColumn('name', 'string')->create(); + + $logTable = new Table('user_log', [], $this->adapter); + $logTable->addColumn('action', 'string')->create(); + + // Create a trigger + $table->createTrigger( + 'log_user_insert', + 'INSERT', + "INSERT INTO user_log (action) VALUES ('user_created')", + ['timing' => 'AFTER'], + )->save(); + + // Drop the trigger + $table->dropTrigger('log_user_insert')->save(); + + // Insert data - trigger should not fire + $table->insert(['name' => 'Bob'])->save(); + + // Verify trigger did not fire + $rows = $this->adapter->fetchAll('SELECT * FROM user_log'); + $this->assertCount(0, $rows); + } + + public function testAddColumnWithAlgorithmInstant(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string') @@ -2857,7 +2961,7 @@ public function testAddColumnWithAlgorithmInstant() $this->assertTrue($this->adapter->hasColumn('users', 'status')); } - public function testAddColumnWithAlgorithmAndLock() + public function testAddColumnWithAlgorithmAndLock(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('name', 'string') @@ -2875,7 +2979,7 @@ public function testAddColumnWithAlgorithmAndLock() $this->assertTrue($this->adapter->hasColumn('products', 'price')); } - public function testChangeColumnWithAlgorithm() + public function testChangeColumnWithAlgorithm(): void { $table = new Table('items', [], $this->adapter); $table->addColumn('description', 'string', ['limit' => 100]) @@ -2895,7 +2999,7 @@ public function testChangeColumnWithAlgorithm() } } - public function testBatchedOperationsWithSameAlgorithm() + public function testBatchedOperationsWithSameAlgorithm(): void { $table = new Table('batch_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2915,7 +3019,7 @@ public function testBatchedOperationsWithSameAlgorithm() $this->assertTrue($this->adapter->hasColumn('batch_test', 'col3')); } - public function testBatchedOperationsWithConflictingAlgorithmsThrowsException() + public function testBatchedOperationsWithConflictingAlgorithmsThrowsException(): void { $table = new Table('conflict_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2935,7 +3039,7 @@ public function testBatchedOperationsWithConflictingAlgorithmsThrowsException() ->update(); } - public function testBatchedOperationsWithConflictingLocksThrowsException() + public function testBatchedOperationsWithConflictingLocksThrowsException(): void { $table = new Table('lock_conflict_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2957,7 +3061,7 @@ public function testBatchedOperationsWithConflictingLocksThrowsException() ->update(); } - public function testInvalidAlgorithmThrowsException() + public function testInvalidAlgorithmThrowsException(): void { $table = new Table('invalid_algo', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2971,7 +3075,7 @@ public function testInvalidAlgorithmThrowsException() ])->update(); } - public function testInvalidLockThrowsException() + public function testInvalidLockThrowsException(): void { $table = new Table('invalid_lock', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2985,7 +3089,7 @@ public function testInvalidLockThrowsException() ])->update(); } - public function testAlgorithmInstantWithExplicitLockThrowsException() + public function testAlgorithmInstantWithExplicitLockThrowsException(): void { $table = new Table('instant_lock_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -3001,7 +3105,7 @@ public function testAlgorithmInstantWithExplicitLockThrowsException() ])->update(); } - public function testAlgorithmConstantsAreDefined() + public function testAlgorithmConstantsAreDefined(): void { $this->assertEquals('DEFAULT', MysqlAdapter::ALGORITHM_DEFAULT); $this->assertEquals('INSTANT', MysqlAdapter::ALGORITHM_INSTANT); @@ -3009,7 +3113,7 @@ public function testAlgorithmConstantsAreDefined() $this->assertEquals('COPY', MysqlAdapter::ALGORITHM_COPY); } - public function testLockConstantsAreDefined() + public function testLockConstantsAreDefined(): void { $this->assertEquals('DEFAULT', MysqlAdapter::LOCK_DEFAULT); $this->assertEquals('NONE', MysqlAdapter::LOCK_NONE); @@ -3017,7 +3121,7 @@ public function testLockConstantsAreDefined() $this->assertEquals('EXCLUSIVE', MysqlAdapter::LOCK_EXCLUSIVE); } - public function testAlgorithmWithMixedCase() + public function testAlgorithmWithMixedCase(): void { $table = new Table('mixed_case', [], $this->adapter); $table->addColumn('col1', 'string') From c8cfb9359cec6a880f67eb25210fb02f260a5bd8 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 12 Mar 2026 22:10:11 +0100 Subject: [PATCH 15/72] 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 694f821b1..dabf3a95e 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 b0014f7c8..9aec4dae7 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 1ab36b5c8..94202d83b 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 fd2dfe91c..0f64e4dd0 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 d7520381d44b5d6341d82876724ad104e66af633 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 15 Mar 2026 04:18:55 +0100 Subject: [PATCH 16/72] Add fluent Table API for check constraints (#1052) * Add fluent Table API for check constraints Add addCheckConstraint() and dropCheckConstraint() methods to the Table class, allowing users to manage check constraints using the same fluent API pattern used for indexes and foreign keys. The adapter already had the underlying implementation, this adds: - AddCheckConstraint action class - DropCheckConstraint action class - Table::addCheckConstraint() method - Table::dropCheckConstraint() method - Table::hasCheckConstraint() method - Plan.php updated to handle check constraint actions - Unit tests for the new methods * docs: Fix check constraint API examples Update examples to use the correct API signature: - addCheckConstraint($expression, $options) not addCheckConstraint($name, $expression) - Replace non-existent checkConstraint() fluent builder with CheckConstraint object - Fix all examples throughout the check constraints section --- docs/en/writing-migrations.rst | 41 +++++++---------- src/Db/Action/AddCheckConstraint.php | 65 +++++++++++++++++++++++++++ src/Db/Action/DropCheckConstraint.php | 43 ++++++++++++++++++ src/Db/Adapter/AbstractAdapter.php | 18 ++++++++ src/Db/Plan/Plan.php | 13 +++++- src/Db/Table.php | 47 +++++++++++++++++++ tests/TestCase/Db/Table/TableTest.php | 41 +++++++++++++++++ 7 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 src/Db/Action/AddCheckConstraint.php create mode 100644 src/Db/Action/DropCheckConstraint.php diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst index 6da4195d1..cea197ff5 100644 --- a/docs/en/writing-migrations.rst +++ b/docs/en/writing-migrations.rst @@ -1547,7 +1547,7 @@ You can add a check constraint to a table using the ``addCheckConstraint()`` met { $table = $this->table('products'); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) - ->addCheckConstraint('price_positive', 'price > 0') + ->addCheckConstraint('price > 0', ['name' => 'price_positive']) ->save(); } @@ -1562,18 +1562,19 @@ You can add a check constraint to a table using the ``addCheckConstraint()`` met } } -The first argument is the constraint name, and the second is the SQL expression -that defines the constraint. The expression should evaluate to a boolean value. +The first argument is the SQL expression that defines the constraint. The expression +should evaluate to a boolean value. The second argument is an options array where +you can specify the constraint ``name``. -Using the CheckConstraint Fluent Builder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Using the CheckConstraint Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For more complex scenarios, you can use the ``checkConstraint()`` method to get -a fluent builder:: +For more complex scenarios, you can create a ``CheckConstraint`` object directly:: addColumn('age', 'integer') ->addColumn('status', 'string', ['limit' => 20]) ->addCheckConstraint( - $this->checkConstraint() - ->setName('age_valid') - ->setExpression('age >= 18 AND age <= 120') + new CheckConstraint('age_valid', 'age >= 18 AND age <= 120') ) ->addCheckConstraint( - $this->checkConstraint() - ->setName('status_valid') - ->setExpression("status IN ('active', 'inactive', 'pending')") + new CheckConstraint('status_valid', "status IN ('active', 'inactive', 'pending')") ) ->save(); } @@ -1602,8 +1599,7 @@ a fluent builder:: Auto-Generated Constraint Names ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you don't specify a constraint name, one will be automatically generated based -on the table name and expression hash:: +If you don't specify a constraint name, one will be automatically generated:: table('inventory'); $table->addColumn('quantity', 'integer') - // Name will be auto-generated like 'inventory_chk_a1b2c3d4' - ->addCheckConstraint( - $this->checkConstraint() - ->setExpression('quantity >= 0') - ) + // Name will be auto-generated + ->addCheckConstraint('quantity >= 0') ->save(); } } @@ -1647,8 +1640,8 @@ Check constraints can reference multiple columns and use complex SQL expressions $table->addColumn('start_date', 'date') ->addColumn('end_date', 'date') ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2]) - ->addCheckConstraint('valid_date_range', 'end_date >= start_date') - ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100') + ->addCheckConstraint('end_date >= start_date', ['name' => 'valid_date_range']) + ->addCheckConstraint('discount BETWEEN 0 AND 100', ['name' => 'valid_discount']) ->save(); } } @@ -1674,7 +1667,7 @@ You can verify if a check constraint exists using the ``hasCheckConstraint()`` m if ($exists) { // do something } else { - $table->addCheckConstraint('price_positive', 'price > 0') + $table->addCheckConstraint('price > 0', ['name' => 'price_positive']) ->save(); } } @@ -1708,7 +1701,7 @@ constraint name:: public function down(): void { $table = $this->table('products'); - $table->addCheckConstraint('price_positive', 'price > 0') + $table->addCheckConstraint('price > 0', ['name' => 'price_positive']) ->save(); } } diff --git a/src/Db/Action/AddCheckConstraint.php b/src/Db/Action/AddCheckConstraint.php new file mode 100644 index 000000000..81b2ebab4 --- /dev/null +++ b/src/Db/Action/AddCheckConstraint.php @@ -0,0 +1,65 @@ +checkConstraint = $checkConstraint; + } + + /** + * Creates a new AddCheckConstraint object after building the check constraint with + * the passed attributes + * + * @param \Migrations\Db\Table\TableMetadata $table The table object to add the check constraint to + * @param string $expression The check constraint expression (e.g., "age >= 18") + * @param array $options Options for the check constraint (e.g., 'name') + * @return self + */ + public static function build( + TableMetadata $table, + string $expression, + array $options = [], + ): self { + $name = $options['name'] ?? ''; + + $checkConstraint = new CheckConstraint($name, $expression); + + return new AddCheckConstraint($table, $checkConstraint); + } + + /** + * Returns the check constraint to be added + * + * @return \Migrations\Db\Table\CheckConstraint + */ + public function getCheckConstraint(): CheckConstraint + { + return $this->checkConstraint; + } +} diff --git a/src/Db/Action/DropCheckConstraint.php b/src/Db/Action/DropCheckConstraint.php new file mode 100644 index 000000000..559cfb19e --- /dev/null +++ b/src/Db/Action/DropCheckConstraint.php @@ -0,0 +1,43 @@ +constraintName = $constraintName; + } + + /** + * Returns the name of the check constraint to drop + * + * @return string + */ + public function getConstraintName(): string + { + return $this->constraintName; + } +} diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 01497ea1c..bcb610588 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -23,6 +23,7 @@ use Exception; use InvalidArgumentException; use Migrations\Config\Config; +use Migrations\Db\Action\AddCheckConstraint; use Migrations\Db\Action\AddColumn; use Migrations\Db\Action\AddForeignKey; use Migrations\Db\Action\AddIndex; @@ -32,6 +33,7 @@ use Migrations\Db\Action\ChangePrimaryKey; use Migrations\Db\Action\CreateTrigger; use Migrations\Db\Action\CreateView; +use Migrations\Db\Action\DropCheckConstraint; use Migrations\Db\Action\DropForeignKey; use Migrations\Db\Action\DropIndex; use Migrations\Db\Action\DropPartition; @@ -1850,6 +1852,22 @@ public function executeActions(TableMetadata $table, array $actions): void )); break; + case $action instanceof AddCheckConstraint: + /** @var \Migrations\Db\Action\AddCheckConstraint $action */ + $instructions->merge($this->getAddCheckConstraintInstructions( + $table, + $action->getCheckConstraint(), + )); + break; + + case $action instanceof DropCheckConstraint: + /** @var \Migrations\Db\Action\DropCheckConstraint $action */ + $instructions->merge($this->getDropCheckConstraintInstructions( + $table->getName(), + $action->getConstraintName(), + )); + break; + default: throw new InvalidArgumentException( sprintf("Don't know how to execute action `%s`", get_class($action)), diff --git a/src/Db/Plan/Plan.php b/src/Db/Plan/Plan.php index 6c4930332..c25ba0a0e 100644 --- a/src/Db/Plan/Plan.php +++ b/src/Db/Plan/Plan.php @@ -9,6 +9,7 @@ namespace Migrations\Db\Plan; use ArrayObject; +use Migrations\Db\Action\AddCheckConstraint; use Migrations\Db\Action\AddColumn; use Migrations\Db\Action\AddForeignKey; use Migrations\Db\Action\AddIndex; @@ -19,6 +20,7 @@ use Migrations\Db\Action\CreateTable; use Migrations\Db\Action\CreateTrigger; use Migrations\Db\Action\CreateView; +use Migrations\Db\Action\DropCheckConstraint; use Migrations\Db\Action\DropForeignKey; use Migrations\Db\Action\DropIndex; use Migrations\Db\Action\DropPartition; @@ -497,7 +499,9 @@ protected function gatherIndexes(array $actions): void } /** - * Collects all foreign key creation and drops from the given intent + * Collects all constraint creation and drops from the given intent + * + * This includes foreign keys and check constraints. * * @param \Migrations\Db\Action\Action[] $actions The actions to parse * @return void @@ -505,7 +509,12 @@ protected function gatherIndexes(array $actions): void protected function gatherConstraints(array $actions): void { foreach ($actions as $action) { - if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) { + if ( + !($action instanceof AddForeignKey) + && !($action instanceof DropForeignKey) + && !($action instanceof AddCheckConstraint) + && !($action instanceof DropCheckConstraint) + ) { continue; } $table = $action->getTable(); diff --git a/src/Db/Table.php b/src/Db/Table.php index 6ba2c036c..59bb1b0c5 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -11,6 +11,7 @@ use Cake\Collection\Collection; use Cake\Core\Configure; use InvalidArgumentException; +use Migrations\Db\Action\AddCheckConstraint; use Migrations\Db\Action\AddColumn; use Migrations\Db\Action\AddForeignKey; use Migrations\Db\Action\AddIndex; @@ -21,6 +22,7 @@ use Migrations\Db\Action\CreateTable; use Migrations\Db\Action\CreateTrigger; use Migrations\Db\Action\CreateView; +use Migrations\Db\Action\DropCheckConstraint; use Migrations\Db\Action\DropForeignKey; use Migrations\Db\Action\DropIndex; use Migrations\Db\Action\DropPartition; @@ -35,6 +37,7 @@ use Migrations\Db\Adapter\MysqlAdapter; use Migrations\Db\Plan\Intent; use Migrations\Db\Plan\Plan; +use Migrations\Db\Table\CheckConstraint; use Migrations\Db\Table\Column; use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; @@ -625,6 +628,50 @@ public function hasForeignKey(string|array $columns, ?string $constraint = null) return $this->getAdapter()->hasForeignKey($this->getName(), $columns, $constraint); } + /** + * Add a check constraint to a database table. + * + * @param string|\Migrations\Db\Table\CheckConstraint $expression The check constraint expression or object + * @param array $options Options for the check constraint (e.g., 'name') + * @return $this + */ + public function addCheckConstraint(string|CheckConstraint $expression, array $options = []) + { + if ($expression instanceof CheckConstraint) { + $action = new AddCheckConstraint($this->table, $expression); + } else { + $action = AddCheckConstraint::build($this->table, $expression, $options); + } + $this->actions->addAction($action); + + return $this; + } + + /** + * Removes the given check constraint from the table. + * + * @param string $constraintName The name of the check constraint to drop + * @return $this + */ + public function dropCheckConstraint(string $constraintName) + { + $action = new DropCheckConstraint($this->table, $constraintName); + $this->actions->addAction($action); + + return $this; + } + + /** + * Checks to see if a check constraint exists. + * + * @param string $constraintName The name of the check constraint + * @return bool + */ + public function hasCheckConstraint(string $constraintName): bool + { + return $this->getAdapter()->hasCheckConstraint($this->getName(), $constraintName); + } + /** * Add partitioning to the table. * diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 1be0f352a..4865e4a11 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -4,9 +4,11 @@ namespace Migrations\Test\TestCase\Db\Table; use InvalidArgumentException; +use Migrations\Db\Action\AddCheckConstraint; use Migrations\Db\Action\AddColumn; use Migrations\Db\Action\AddForeignKey; use Migrations\Db\Action\AddIndex; +use Migrations\Db\Action\DropCheckConstraint; use Migrations\Db\Action\DropIndex; use Migrations\Db\Adapter\AdapterInterface; use Migrations\Db\Adapter\MysqlAdapter; @@ -14,6 +16,7 @@ use Migrations\Db\Adapter\SqliteAdapter; use Migrations\Db\Adapter\SqlserverAdapter; use Migrations\Db\Table; +use Migrations\Db\Table\CheckConstraint; use Migrations\Db\Table\Column; use Migrations\Db\Table\ForeignKey; use Migrations\Db\Table\Index; @@ -521,6 +524,44 @@ public static function removeIndexDataprovider() ]; } + public function testAddCheckConstraintWithExpression(): void + { + $adapter = new MysqlAdapter([]); + $table = new Table('ntable', [], $adapter); + $table->addCheckConstraint('age >= 18', ['name' => 'age_check']); + + $actions = $this->getPendingActions($table); + $this->assertInstanceOf(AddCheckConstraint::class, $actions[0]); + $constraint = $actions[0]->getCheckConstraint(); + $this->assertSame('age_check', $constraint->getName()); + $this->assertSame('age >= 18', $constraint->getExpression()); + } + + public function testAddCheckConstraintWithObject(): void + { + $adapter = new MysqlAdapter([]); + $table = new Table('ntable', [], $adapter); + $constraint = new CheckConstraint('price_positive', 'price > 0'); + $table->addCheckConstraint($constraint); + + $actions = $this->getPendingActions($table); + $this->assertInstanceOf(AddCheckConstraint::class, $actions[0]); + $this->assertSame($constraint, $actions[0]->getCheckConstraint()); + $this->assertSame('price_positive', $actions[0]->getCheckConstraint()->getName()); + $this->assertSame('price > 0', $actions[0]->getCheckConstraint()->getExpression()); + } + + public function testDropCheckConstraint(): void + { + $adapter = new MysqlAdapter([]); + $table = new Table('ntable', [], $adapter); + $table->dropCheckConstraint('age_check'); + + $actions = $this->getPendingActions($table); + $this->assertInstanceOf(DropCheckConstraint::class, $actions[0]); + $this->assertSame('age_check', $actions[0]->getConstraintName()); + } + protected function getPendingActions($table) { $prop = new ReflectionProperty(get_class($table), 'actions'); From 4e108a357fe14c48cb46d27bbedabe960df693b2 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 15 Mar 2026 16:34:11 +0100 Subject: [PATCH 17/72] fix CS (#1054) --- .gitignore | 1 + composer.json | 4 ++-- phpcs.xml | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1cb7676d0..7d1294ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ error.log /tests/test_app/config/TestsMigrations/schema-dump-test.lock /tests/test_app/Plugin/TestBlog/config/Migrations/* .phpunit.cache +.phpcs.cache .ddev # IDE and editor specific files # diff --git a/composer.json b/composer.json index 1c3779aa3..2f2b54396 100644 --- a/composer.json +++ b/composer.json @@ -68,8 +68,8 @@ "@stan", "@test" ], - "cs-check": "phpcs --parallel=16 -p", - "cs-fix": "phpcbf --parallel=16 -p", + "cs-check": "phpcs", + "cs-fix": "phpcbf", "phpstan": "tools/phpstan analyse", "stan": "@phpstan", "stan-baseline": "tools/phpstan --generate-baseline", diff --git a/phpcs.xml b/phpcs.xml index 7a7a8a87c..e89659f62 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,9 @@ - + + + + src/ tests/ From 9b190c1f869a78b87b8efc2eff33194e335b8370 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Tue, 17 Mar 2026 15:35:58 +0100 Subject: [PATCH 18/72] Update CI workflow branches for GitHub Actions --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d2da9cfc..da813c68a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,10 @@ name: CI on: push: branches: - - 3.x - 4.x - 4.next - 5.x + - 5.next pull_request: branches: - '*' From e485847fe30572832788d33ce2c70d96f07ef431 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Fri, 27 Mar 2026 19:11:07 +0100 Subject: [PATCH 19/72] improve error message if a migration contains the old base migration class (#1059) --- src/Migration/Manager.php | 29 ++++++++++++++++++- tests/TestCase/Migration/ManagerTest.php | 12 ++++++++ ...20260327000000_LegacyAbstractMigration.php | 11 +++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 4a9f1886d..6baad31e8 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -975,7 +975,8 @@ function ($phpFile) { $io->verbose("Loading class $class from $filePath."); - // load the migration file + $this->checkMigrationClass($filePath); + $orig_display_errors_setting = ini_get('display_errors'); ini_set('display_errors', 'On'); @@ -1025,6 +1026,32 @@ function ($phpFile) { return (array)$this->migrations; } + /** + * Prevent fatal errors when loading legacy migration files that still reference old classes + * + * @param string $filePath Migration file path + * @return void + */ + protected function checkMigrationClass(string $filePath): void + { + $contents = file_get_contents($filePath); + if ($contents === false) { + return; + } + + $usesLegacyAbstractMigration = + str_contains($contents, 'use Migrations\AbstractMigration;') || + str_contains($contents, 'extends AbstractMigration') || + str_contains($contents, 'extends \Migrations\AbstractMigration'); + + if ($usesLegacyAbstractMigration) { + throw new RuntimeException(sprintf( + 'Migration file `%s` uses the legacy `Migrations\\AbstractMigration` class, which is not available in this version. Update the migration to extend `Migrations\\BaseMigration`.', + $filePath, + )); + } + } + /** * Returns a list of migration files found in the provided migration paths. * diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index ba7c324dd..4efcf70c2 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -639,6 +639,18 @@ public function testGetMigrationsWithInvalidMigrationClassName() $manager->getMigrations(); } + public function testGetMigrationsWithLegacyAbstractMigrationClass(): void + { + $config = new Config(['paths' => ['migrations' => ROOT . '/config/LegacyAbstractMigration']]); + $manager = new Manager($config, $this->io); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessageMatches('/uses the legacy `Migrations\\\\AbstractMigration` class/'); + $this->expectExceptionMessageMatches('/20260327000000_LegacyAbstractMigration\\.php/'); + + $manager->getMigrations(); + } + public function testGetMigrationsWithAnonymousClass() { $config = new Config(['paths' => ['migrations' => ROOT . '/config/AnonymousMigrations']]); diff --git a/tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php b/tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php new file mode 100644 index 000000000..c7e43eb88 --- /dev/null +++ b/tests/test_app/config/LegacyAbstractMigration/20260327000000_LegacyAbstractMigration.php @@ -0,0 +1,11 @@ + Date: Sat, 28 Mar 2026 05:06:52 +0100 Subject: [PATCH 20/72] Add migrations reset command (#1051) * Add migrations reset command for development workflow Implements the "nuclear option" for migrations as discussed in #972: - Drops all application tables (excluding migration tracking tables) - Re-runs all migrations from scratch - Requires interactive Y/N confirmation for safety (default: N) - Supports --dry-run mode to preview changes - Dispatches Migration.beforeReset and Migration.afterReset events This is useful during development when you want a fresh database without manually rolling back or dropping tables. Refs #972 * Fix foreign key handling for PostgreSQL and SQL Server - PostgreSQL: Use CASCADE in DROP TABLE statement - SQL Server: Drop foreign key constraints first before dropping tables - MySQL/SQLite: Continue using session-level FK check toggle * Update completion test to include reset command * Improve reset command logic - Remove sessions from protected tables (true nuclear option) - Rename protectedTables to trackingTables for clarity - Clear seed records (cake_seeds) along with migration records - Rename clearMigrationRecords to clearTrackingRecords * Simplify reset command to drop all tables Instead of preserving tracking tables and clearing their records, just drop everything and let migrations recreate the tables. * Use instanceof for driver type checking and extract helper method - Replace string operations on class names with instanceof checks - Extract runMigrationsAndDispatch() to reduce code duplication * Move FK constraint handling to adapter layer - Add disableForeignKeyConstraints/enableForeignKeyConstraints to AdapterInterface - Implement FK methods in MysqlAdapter (SET FOREIGN_KEY_CHECKS) - Implement FK methods in SqliteAdapter (PRAGMA foreign_keys) - Implement FK methods in SqlserverAdapter (drop all FK constraints) - PostgresAdapter uses no-op methods with CASCADE in dropTable - Add FK methods to AdapterWrapper for delegation - Refactor ResetCommand to use adapter methods instead of dialect-specific code * Add FK constraint methods to test trait --- src/Command/ResetCommand.php | 282 ++++++++++++++++++ src/Db/Adapter/AdapterInterface.php | 20 ++ src/Db/Adapter/AdapterWrapper.php | 16 + src/Db/Adapter/MysqlAdapter.php | 16 + src/Db/Adapter/PostgresAdapter.php | 20 +- src/Db/Adapter/SqliteAdapter.php | 16 + src/Db/Adapter/SqlserverAdapter.php | 34 +++ src/MigrationsPlugin.php | 2 + tests/TestCase/Command/CompletionTest.php | 4 +- tests/TestCase/Command/ResetCommandTest.php | 183 ++++++++++++ .../Db/Adapter/DefaultAdapterTrait.php | 8 + 11 files changed, 598 insertions(+), 3 deletions(-) create mode 100644 src/Command/ResetCommand.php create mode 100644 tests/TestCase/Command/ResetCommandTest.php diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php new file mode 100644 index 000000000..db0067356 --- /dev/null +++ b/src/Command/ResetCommand.php @@ -0,0 +1,282 @@ + + */ + use EventDispatcherTrait; + + /** + * The default name added to the application command list + * + * @return string + */ + public static function defaultName(): string + { + return 'migrations reset'; + } + + /** + * Configure the option parser + * + * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure + * @return \Cake\Console\ConsoleOptionParser + */ + public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + { + $parser->setDescription([ + 'Drop all tables and re-run all migrations.', + '', + 'This is a destructive operation!', + 'All data in the database will be lost.', + '', + 'migrations reset', + 'migrations reset -c secondary', + 'migrations reset --dry-run', + ])->addOption('plugin', [ + 'short' => 'p', + 'help' => 'The plugin to run migrations for', + ])->addOption('connection', [ + 'short' => 'c', + 'help' => 'The datasource connection to use', + 'default' => 'default', + ])->addOption('source', [ + 'short' => 's', + 'default' => ConfigInterface::DEFAULT_MIGRATION_FOLDER, + 'help' => 'The folder where your migrations are', + ])->addOption('dry-run', [ + 'short' => 'x', + 'help' => 'Preview what tables would be dropped without making changes', + 'boolean' => true, + ])->addOption('no-lock', [ + 'help' => 'If present, no lock file will be generated after migrating', + 'boolean' => true, + ]); + + return $parser; + } + + /** + * Execute the command. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int|null The exit code or null for success + */ + public function execute(Arguments $args, ConsoleIo $io): ?int + { + $event = $this->dispatchEvent('Migration.beforeReset'); + if ($event->isStopped()) { + return $event->getResult() ? self::CODE_SUCCESS : self::CODE_ERROR; + } + + $connectionName = (string)$args->getOption('connection'); + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get($connectionName); + $dryRun = (bool)$args->getOption('dry-run'); + + if ($dryRun) { + $io->out('DRY-RUN mode enabled - no changes will be made'); + $io->out(''); + } + + // Get tables to drop + $tablesToDrop = $this->getTablesToDrop($connection); + + if (empty($tablesToDrop)) { + $io->out('No tables to drop.'); + $io->out(''); + $io->out('Running migrations...'); + + return $this->runMigrationsAndDispatch($args, $io); + } + + // Show what will be dropped + $io->out('The following tables will be dropped:'); + foreach ($tablesToDrop as $table) { + $io->out(' - ' . $table); + } + $io->out(''); + + // Ask for confirmation (unless dry-run) + if (!$dryRun) { + $continue = $io->askChoice( + 'This will permanently delete all data. Do you want to continue?', + ['y', 'n'], + 'n', + ); + if ($continue !== 'y') { + $io->warning('Reset operation aborted.'); + + return self::CODE_SUCCESS; + } + } + + // Drop tables + $io->out(''); + if (!$dryRun) { + $factory = new ManagerFactory([ + 'plugin' => $args->getOption('plugin'), + 'source' => $args->getOption('source'), + 'connection' => $args->getOption('connection'), + ]); + $manager = $factory->createManager($io); + $adapter = $manager->getEnvironment()->getAdapter(); + + $this->dropTables($adapter, $tablesToDrop, $io); + } else { + $io->info('DRY-RUN: Would drop ' . count($tablesToDrop) . ' table(s).'); + } + + $io->out(''); + + // Re-run migrations + if (!$dryRun) { + return $this->runMigrationsAndDispatch($args, $io); + } + + $io->info('DRY-RUN: Would re-run all migrations.'); + + return self::CODE_SUCCESS; + } + + /** + * Get list of tables to drop. + * + * @param \Cake\Database\Connection $connection Database connection + * @return array List of table names + */ + protected function getTablesToDrop(Connection $connection): array + { + $schema = $connection->getDriver()->schemaDialect(); + + return $schema->listTables(); + } + + /** + * Drop tables with foreign key handling. + * + * @param \Migrations\Db\Adapter\AdapterInterface $adapter The adapter + * @param array $tables Tables to drop + * @param \Cake\Console\ConsoleIo $io Console IO + * @return void + */ + protected function dropTables(AdapterInterface $adapter, array $tables, ConsoleIo $io): void + { + if (!$adapter instanceof DirectActionInterface) { + throw new RuntimeException('The adapter must implement DirectActionInterface'); + } + + $adapter->disableForeignKeyConstraints(); + + try { + foreach ($tables as $table) { + $io->verbose("Dropping table: {$table}"); + $adapter->dropTable($table); + } + } finally { + $adapter->enableForeignKeyConstraints(); + } + + $io->success('Dropped ' . count($tables) . ' table(s).'); + } + + /** + * Run migrations and dispatch afterReset event. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int|null The exit code + */ + protected function runMigrationsAndDispatch(Arguments $args, ConsoleIo $io): ?int + { + $result = $this->runMigrations($args, $io); + $this->dispatchEvent('Migration.afterReset'); + + return $result; + } + + /** + * Run migrations. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io + * @return int|null The exit code + */ + protected function runMigrations(Arguments $args, ConsoleIo $io): ?int + { + $factory = new ManagerFactory([ + 'plugin' => $args->getOption('plugin'), + 'source' => $args->getOption('source'), + 'connection' => $args->getOption('connection'), + 'dry-run' => (bool)$args->getOption('dry-run'), + ]); + + $manager = $factory->createManager($io); + $config = $manager->getConfig(); + + $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using paths ' . $config->getMigrationPath()); + + try { + $start = microtime(true); + $manager->migrate(null, false, null); + $end = microtime(true); + } catch (Throwable $e) { + $io->err('' . $e->getMessage() . ''); + $io->verbose($e->getTraceAsString()); + + return self::CODE_ERROR; + } + + $io->comment('All Done. Took ' . sprintf('%.4fs', $end - $start)); + $io->out(''); + + $exitCode = self::CODE_SUCCESS; + + // Run dump command to generate lock file + if (!$args->getOption('no-lock') && !$args->getOption('dry-run')) { + $io->verbose(''); + $io->verbose('Dumping the current schema of the database to be used while baking a diff'); + $io->verbose(''); + + $newArgs = DumpCommand::extractArgs($args); + $exitCode = $this->executeCommand(DumpCommand::class, $newArgs, $io); + } + + return $exitCode; + } +} diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 0dad1ca37..c1c011ee3 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -553,6 +553,26 @@ public function createTable(TableMetadata $table, array $columns = [], array $in */ public function truncateTable(string $tableName): void; + /** + * Disable foreign key constraint checking. + * + * This is useful when dropping tables or performing bulk operations + * that would otherwise fail due to foreign key constraints. + * + * @return void + */ + public function disableForeignKeyConstraints(): void; + + /** + * Enable foreign key constraint checking. + * + * This should be called after disableForeignKeyConstraints() to + * restore normal constraint checking behavior. + * + * @return void + */ + public function enableForeignKeyConstraints(): void; + /** * Returns table columns * diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php index 597e9926d..054f547f7 100644 --- a/src/Db/Adapter/AdapterWrapper.php +++ b/src/Db/Adapter/AdapterWrapper.php @@ -590,4 +590,20 @@ public function getSchemaTableName(): string { return $this->getAdapter()->getSchemaTableName(); } + + /** + * @inheritDoc + */ + public function disableForeignKeyConstraints(): void + { + $this->getAdapter()->disableForeignKeyConstraints(); + } + + /** + * @inheritDoc + */ + public function enableForeignKeyConstraints(): void + { + $this->getAdapter()->enableForeignKeyConstraints(); + } } diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 7e3f38729..e55d18847 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -560,6 +560,22 @@ public function truncateTable(string $tableName): void $this->execute($sql); } + /** + * @inheritDoc + */ + public function disableForeignKeyConstraints(): void + { + $this->execute('SET FOREIGN_KEY_CHECKS = 0'); + } + + /** + * @inheritDoc + */ + public function enableForeignKeyConstraints(): void + { + $this->execute('SET FOREIGN_KEY_CHECKS = 1'); + } + /** * Convert from cakephp/database conventions to migrations\column * diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 31e77135a..ea2f1ac28 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -333,7 +333,7 @@ protected function getRenameTableInstructions(string $tableName, string $newTabl protected function getDropTableInstructions(string $tableName): AlterInstructions { $this->removeCreatedTable($tableName); - $sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName)); + $sql = sprintf('DROP TABLE %s CASCADE', $this->quoteTableName($tableName)); return new AlterInstructions([], [$sql]); } @@ -351,6 +351,24 @@ public function truncateTable(string $tableName): void $this->execute($sql); } + /** + * @inheritDoc + */ + public function disableForeignKeyConstraints(): void + { + // PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks. + // This method is a no-op for PostgreSQL since dropTable already uses CASCADE. + } + + /** + * @inheritDoc + */ + public function enableForeignKeyConstraints(): void + { + // PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks. + // This method is a no-op for PostgreSQL. + } + /** * @inheritDoc */ diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index a0d599d95..28f6861ae 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -408,6 +408,22 @@ public function truncateTable(string $tableName): void } } + /** + * @inheritDoc + */ + public function disableForeignKeyConstraints(): void + { + $this->execute('PRAGMA foreign_keys = OFF'); + } + + /** + * @inheritDoc + */ + public function enableForeignKeyConstraints(): void + { + $this->execute('PRAGMA foreign_keys = ON'); + } + /** * Parses a default-value expression to yield either a Literal representing * a string value, a string representing an expression, or some other scalar diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index d961e58ba..0f500d67a 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -280,6 +280,40 @@ public function truncateTable(string $tableName): void $this->execute($sql); } + /** + * @inheritDoc + */ + public function disableForeignKeyConstraints(): void + { + // SQL Server doesn't support disabling FK checks globally. + // We drop all foreign key constraints instead. + $sql = "SELECT + fk.name AS constraint_name, + SCHEMA_NAME(t.schema_id) AS schema_name, + t.name AS table_name + FROM sys.foreign_keys fk + INNER JOIN sys.tables t ON fk.parent_object_id = t.object_id + WHERE SCHEMA_NAME(t.schema_id) = ?"; + + $rows = $this->query($sql, [$this->schema])->fetchAll('assoc'); + + foreach ($rows as $row) { + $constraintName = $this->quoteColumnName($row['constraint_name']); + $tableName = $this->quoteTableName($row['table_name']); + $this->execute("ALTER TABLE {$tableName} DROP CONSTRAINT {$constraintName}"); + } + } + + /** + * @inheritDoc + */ + public function enableForeignKeyConstraints(): void + { + // SQL Server FK constraints were dropped, not disabled. + // They would need to be recreated, but after a reset/drop operation + // the tables will be recreated with their constraints by migrations. + } + /** * @param string $tableName Table name * @param ?string $columnName Column name diff --git a/src/MigrationsPlugin.php b/src/MigrationsPlugin.php index b627f1cd4..f0fbded80 100644 --- a/src/MigrationsPlugin.php +++ b/src/MigrationsPlugin.php @@ -26,6 +26,7 @@ use Migrations\Command\EntryCommand; use Migrations\Command\MarkMigratedCommand; use Migrations\Command\MigrateCommand; +use Migrations\Command\ResetCommand; use Migrations\Command\RollbackCommand; use Migrations\Command\SeedCommand; use Migrations\Command\SeedResetCommand; @@ -73,6 +74,7 @@ public function console(CommandCollection $commands): CommandCollection DumpCommand::class, MarkMigratedCommand::class, MigrateCommand::class, + ResetCommand::class, RollbackCommand::class, StatusCommand::class, ]; diff --git a/tests/TestCase/Command/CompletionTest.php b/tests/TestCase/Command/CompletionTest.php index b05f0a4da..78520c4f0 100644 --- a/tests/TestCase/Command/CompletionTest.php +++ b/tests/TestCase/Command/CompletionTest.php @@ -47,11 +47,11 @@ public function testMigrationsSubcommands() // Upgrade command is hidden when legacyTables is disabled if (Configure::read('Migrations.legacyTables') === false) { $expected = [ - 'dump mark_migrated migrate rollback status', + 'dump mark_migrated migrate reset rollback status', ]; } else { $expected = [ - 'dump mark_migrated migrate rollback status upgrade', + 'dump mark_migrated migrate reset rollback status upgrade', ]; } $actual = $this->_out->messages(); diff --git a/tests/TestCase/Command/ResetCommandTest.php b/tests/TestCase/Command/ResetCommandTest.php new file mode 100644 index 000000000..81a713ea1 --- /dev/null +++ b/tests/TestCase/Command/ResetCommandTest.php @@ -0,0 +1,183 @@ +clearMigrationRecords('test'); + $this->clearMigrationRecords('test', 'Migrator'); + + // Reset event manager to avoid pollution from other tests + EventManager::instance()->off('Migration.beforeReset'); + EventManager::instance()->off('Migration.afterReset'); + } + + public function tearDown(): void + { + parent::tearDown(); + foreach ($this->createdFiles as $file) { + unlink($file); + } + + // Clean up event listeners + EventManager::instance()->off('Migration.beforeReset'); + EventManager::instance()->off('Migration.afterReset'); + } + + protected function resetOutput(): void + { + if ($this->_out) { + $property = new ReflectionProperty($this->_out, '_out'); + $property->setValue($this->_out, []); + } + $this->_in = null; + } + + public function testHelp(): void + { + $this->exec('migrations reset --help'); + + $this->assertExitSuccess(); + $this->assertOutputContains('Drop all tables and re-run all migrations'); + $this->assertOutputContains('destructive operation'); + } + + public function testDryRunWithTables(): void + { + // The test db has fixture tables, so there will be tables to drop + $this->exec('migrations reset -c test --dry-run --no-lock'); + $this->assertExitSuccess(); + + $this->assertOutputContains('DRY-RUN mode enabled'); + $this->assertOutputContains('DRY-RUN: Would drop'); + $this->assertOutputContains('DRY-RUN: Would re-run all migrations'); + } + + public function testResetAborted(): void + { + // Test aborting the reset (fixture tables exist) + $this->exec('migrations reset -c test --no-lock', ['n']); + $this->assertExitSuccess(); + + $this->assertOutputContains('The following tables will be dropped'); + $this->assertErrorContains('Reset operation aborted'); + } + + public function testResetConfirmed(): void + { + // First run migrations to create some tables + $this->exec('migrations migrate -c test --no-lock'); + $this->assertExitSuccess(); + $this->resetOutput(); + + // Test confirming the reset + $this->exec('migrations reset -c test --no-lock', ['y']); + $this->assertExitSuccess(); + + $this->assertOutputContains('The following tables will be dropped'); + $this->assertOutputContains('Dropped'); + $this->assertOutputContains('All Done'); + } + + public function testResetWithLock(): void + { + $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; + + // First run migrations + $this->exec('migrations migrate -c test --no-lock'); + $this->assertExitSuccess(); + $this->resetOutput(); + + // Test reset with lock file generation + $this->exec('migrations reset -c test', ['y']); + $this->assertExitSuccess(); + + $this->assertOutputContains('All Done'); + + $dumpFile = $migrationPath . DS . 'schema-dump-test.lock'; + $this->createdFiles[] = $dumpFile; + $this->assertFileExists($dumpFile); + } + + public function testEventsFired(): void + { + // First create something to reset + $this->exec('migrations migrate -c test --no-lock'); + $this->assertExitSuccess(); + $this->resetOutput(); + + // Clean up any existing listeners before registering test listeners + EventManager::instance()->off('Migration.beforeReset'); + EventManager::instance()->off('Migration.afterReset'); + + /** @var array $fired */ + $fired = []; + EventManager::instance()->on('Migration.beforeReset', function (EventInterface $event) use (&$fired): void { + $fired[] = $event->getName(); + }); + EventManager::instance()->on('Migration.afterReset', function (EventInterface $event) use (&$fired): void { + $fired[] = $event->getName(); + }); + + $this->exec('migrations reset -c test --no-lock', ['y']); + $this->assertExitSuccess(); + $this->assertSame(['Migration.beforeReset', 'Migration.afterReset'], $fired); + } + + public function testBeforeResetEventAbort(): void + { + // First create something to reset + $this->exec('migrations migrate -c test --no-lock'); + $this->assertExitSuccess(); + $this->resetOutput(); + + /** @var array $fired */ + $fired = []; + EventManager::instance()->on('Migration.beforeReset', function (EventInterface $event) use (&$fired): void { + $fired[] = $event->getName(); + $event->stopPropagation(); + $event->setResult(0); + }); + EventManager::instance()->on('Migration.afterReset', function (EventInterface $event) use (&$fired): void { + $fired[] = $event->getName(); + }); + + $this->exec('migrations reset -c test --no-lock', ['y']); + $this->assertExitError(); + + // Only one event was fired + $this->assertSame(['Migration.beforeReset'], $fired); + } + + public function testResetWithPlugin(): void + { + $this->loadPlugins(['Migrator']); + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + $connection->execute('DROP TABLE IF EXISTS migrator'); + + // Run plugin migrations + $this->exec('migrations migrate -c test --plugin Migrator --no-lock'); + $this->assertExitSuccess(); + $this->resetOutput(); + + // Reset with plugin option + $this->exec('migrations reset -c test --plugin Migrator --no-lock', ['y']); + $this->assertExitSuccess(); + + $this->assertOutputContains('All Done'); + } +} diff --git a/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php b/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php index e92a59656..59ecb01f4 100644 --- a/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php +++ b/tests/TestCase/Db/Adapter/DefaultAdapterTrait.php @@ -209,4 +209,12 @@ protected function getDropTriggerInstructions(string $tableName, string $trigger { return new AlterInstructions(); } + + public function disableForeignKeyConstraints(): void + { + } + + public function enableForeignKeyConstraints(): void + { + } } From e9baed9a39efdab64288832c4f1984d98a484ccb Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 29 Mar 2026 06:06:25 +0200 Subject: [PATCH 21/72] Fix checkSync() to only consider migrations from current context (#1061) When using the unified `cake_migrations` table, `BakeMigrationDiffCommand::checkSync()` was comparing the last migrated version (from ALL contexts including plugins) against the last migration file in the current context (app or specific plugin). This caused false "not in sync" errors when a plugin had more recent migrations than the app being baked. The fix filters migrated versions to only consider those that have corresponding files in the current context before comparing. Fixes #1060 --- src/Command/BakeMigrationDiffCommand.php | 60 +++++++++++++++---- .../Command/BakeMigrationDiffCommandTest.php | 47 +++++++++++++++ 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index d0490dafe..920af9371 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -33,6 +33,7 @@ use Cake\Event\Event; use Cake\Event\EventManager; use Error; +use Migrations\Db\Adapter\UnifiedMigrationsTableStorage; use Migrations\Migration\ManagerFactory; use Migrations\Util\TableFinder; use Migrations\Util\UtilTrait; @@ -169,13 +170,18 @@ protected function setup(Arguments $args): void $migratedItems = []; if ($tableExists) { - $query = $connection->selectQuery(); - /** @var array $migratedItems */ - $migratedItems = $query + $query = $connection->selectQuery() ->select(['version']) ->from($this->phinxTable) - ->orderBy(['version DESC']) - ->execute()->fetchAll('assoc'); + ->orderBy(['version DESC']); + + // In unified table mode, filter by the current plugin context + if ($this->phinxTable === UnifiedMigrationsTableStorage::TABLE_NAME) { + $query->where(['plugin IS' => $this->plugin]); + } + + /** @var array $migratedItems */ + $migratedItems = $query->execute()->fetchAll('assoc'); } $this->migratedItems = $migratedItems; @@ -484,22 +490,56 @@ protected function getIndexes(): void /** * Checks that the migrations history is in sync with the migrations files * + * Compares the last migrated version against the last migration file, + * but only considers migrations that belong to the current context (app or plugin). + * This avoids false positives when migrations from other plugins have been + * run more recently than the last app migration. + * + * @see https://github.com/cakephp/migrations/issues/1060 * @return bool Whether migrations history is sync or not */ protected function checkSync(): bool { + // No files and no migrations - nothing to check if (!$this->migrationsFiles && !$this->migratedItems) { return true; } - if ($this->migratedItems) { - $lastVersion = $this->migratedItems[0]['version']; - $lastFile = end($this->migrationsFiles); + // No files in this context - nothing to sync + // This allows baking a snapshot for a new plugin even when + // the unified migrations table has records from other contexts + if (!$this->migrationsFiles) { + return true; + } - return $lastFile && str_contains($lastFile, (string)$lastVersion); + // Have files - need to verify they're synced + // Extract version numbers from current context's migration files + $fileVersions = []; + foreach ($this->migrationsFiles as $file) { + $filename = basename($file); + if (preg_match('/^(\d+)_/', $filename, $matches)) { + $fileVersions[] = $matches[1]; + } + } + + // Filter migrated versions to only those that have corresponding files in this context + $contextMigratedVersions = []; + foreach ($this->migratedItems as $item) { + if (in_array((string)$item['version'], $fileVersions, true)) { + $contextMigratedVersions[] = (string)$item['version']; + } } - return false; + if (empty($contextMigratedVersions)) { + // No migrations from this context have been run yet + return false; + } + + // Get the most recent migrated version within this context + $lastVersion = max($contextMigratedVersions); + $lastFile = end($this->migrationsFiles); + + return $lastFile && str_contains($lastFile, $lastVersion); } /** diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 2dc4d388a..963571e17 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -142,6 +142,53 @@ public function testHistoryNotInSync() $this->assertExitCode(BaseCommand::CODE_ERROR); } + /** + * Tests that baking a diff succeeds when app migrations are in sync + * even if a plugin has more recent migrations. + * + * This test specifically tests the unified table mode where all migrations + * (app and plugins) are stored in a single cake_migrations table. + * + * @see https://github.com/cakephp/migrations/issues/1060 + * @return void + */ + public function testCheckSyncWithPluginMigrationsMoreRecent(): void + { + // This bug only affects unified table mode + Configure::write('Migrations.legacyTables', false); + + // Run app migrations first to get them in sync + $this->exec('migrations migrate -c test --no-lock'); + $this->assertExitSuccess(); + + // Create a schema dump file (required for baking a diff) + $this->exec('migrations dump -c test'); + $this->assertExitSuccess(); + + // Insert a more recent migration record for a plugin + // This simulates having a plugin migration that was run after the last app migration + $this->insertMigrationRecord('test', 99999999999999, 'PluginMigration', 'SomePlugin'); + + // Now try to bake a diff for the app - this should succeed + // because all app migration files have been migrated + $this->exec('bake migration_diff CheckSyncTest -c test'); + + // Should not contain the "not in sync" error + $this->assertOutputNotContains('Your migrations history is not in sync'); + $this->assertExitSuccess(); + + // Clean up generated files + $path = ROOT . DS . 'config' . DS . 'Migrations' . DS; + $this->generatedFiles = array_merge( + $this->generatedFiles, + glob($path . '*_CheckSyncTest.php') ?: [], + ); + $this->generatedFiles[] = $path . 'schema-dump-test.lock'; + + // Restore legacy table mode + Configure::delete('Migrations.legacyTables'); + } + /** * Tests that baking a diff while history is empty and no migration files exists * will fall back to baking a snapshot From 36aa17089ab500e6c6877f874df77aec2058006c Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sun, 29 Mar 2026 15:33:38 +0200 Subject: [PATCH 22/72] Introduce VitePress documentation using @cakephp/docs-skeleton (#1055) * initial vitepress conversion * clean up version switcher * add Dockerfile for docs build step * docs: Add anonymous migration classes documentation (#1056) Document the --style anonymous option for bake migration including: - Benefits of anonymous migration classes - Command usage example - Generated code structure - Global Migrations.style configuration option * Fix prev verion urls Use shorthand npm package syntax * Don't run CI on doc changes * adjust docs deploy --------- Co-authored-by: Kevin Pfeifer Co-authored-by: Mark Scherer --- .github/workflows/ci.yml | 4 + .github/workflows/deploy_docs_5x.yml | 1 + Dockerfile | 61 +- docs.Dockerfile | 26 - docs/.gitignore | 3 + docs/.vitepress/config.js | 51 + docs/.vitepress/theme/index.js | 1 + docs/.vitepress/toc_en.json | 23 + docs/config/__init__.py | 0 docs/config/all.py | 53 - docs/en/conf.py | 9 - docs/en/contents.md | 8 + docs/en/contents.rst | 10 - docs/en/executing-queries.md | 135 ++ docs/en/executing-queries.rst | 134 -- docs/en/index.md | 941 ++++++++ docs/en/seeding.md | 585 +++++ docs/en/upgrading-to-builtin-backend.md | 187 ++ docs/en/upgrading-to-builtin-backend.rst | 198 -- docs/en/upgrading.md | 162 ++ docs/en/using-the-query-builder.md | 260 +++ docs/en/using-the-query-builder.rst | 272 --- docs/en/writing-migrations.md | 2527 ++++++++++++++++++++++ docs/en/writing-migrations.rst | 2458 --------------------- docs/package-lock.json | 2104 ++++++++++++++++++ docs/package.json | 19 + 26 files changed, 7047 insertions(+), 3185 deletions(-) delete mode 100644 docs.Dockerfile create mode 100644 docs/.gitignore create mode 100644 docs/.vitepress/config.js create mode 100644 docs/.vitepress/theme/index.js create mode 100644 docs/.vitepress/toc_en.json delete mode 100644 docs/config/__init__.py delete mode 100644 docs/config/all.py delete mode 100644 docs/en/conf.py create mode 100644 docs/en/contents.md delete mode 100644 docs/en/contents.rst create mode 100644 docs/en/executing-queries.md delete mode 100644 docs/en/executing-queries.rst create mode 100644 docs/en/index.md create mode 100644 docs/en/seeding.md create mode 100644 docs/en/upgrading-to-builtin-backend.md delete mode 100644 docs/en/upgrading-to-builtin-backend.rst create mode 100644 docs/en/upgrading.md create mode 100644 docs/en/using-the-query-builder.md delete mode 100644 docs/en/using-the-query-builder.rst create mode 100644 docs/en/writing-migrations.md delete mode 100644 docs/en/writing-migrations.rst create mode 100644 docs/package-lock.json create mode 100644 docs/package.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d2da9cfc..e551da118 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,13 @@ on: - 4.x - 4.next - 5.x + paths-ignore: + - docs/** pull_request: branches: - '*' + paths-ignore: + - docs/** workflow_dispatch: permissions: diff --git a/.github/workflows/deploy_docs_5x.yml b/.github/workflows/deploy_docs_5x.yml index bc6f5ad9c..2f5c1a27e 100644 --- a/.github/workflows/deploy_docs_5x.yml +++ b/.github/workflows/deploy_docs_5x.yml @@ -21,3 +21,4 @@ jobs: with: git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-5' ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} + branch: '5.x' diff --git a/Dockerfile b/Dockerfile index 70ceb413d..832f6bc1c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,36 @@ -# Basic docker based environment -# Necessary to trick dokku into building the documentation -# using dockerfile instead of herokuish -FROM ubuntu:22.04 - -# Add basic tools -RUN apt-get update && \ - apt-get install -y build-essential \ - software-properties-common \ - curl \ - git \ - libxml2 \ - libffi-dev \ - libssl-dev && \ - LC_ALL=C.UTF-8 add-apt-repository ppa:ondrej/php && \ - apt-get update && \ - apt-get install -y php8.1-cli php8.1-mbstring php8.1-xml php8.1-zip php8.1-intl php8.1-opcache php8.1-sqlite &&\ - apt-get clean &&\ - rm -rf /var/lib/apt/lists/* - -WORKDIR /code - -VOLUME ["/code"] - -CMD [ '/bin/bash' ] +# ---------------------- +# 1. Build stage +# ---------------------- +FROM node:22-alpine AS builder + +# Git is required because docs/package.json pulls a dependency from GitHub. +RUN apk add --no-cache git openssh-client + +WORKDIR /app/docs + +# Copy dependency manifests first to preserve Docker layer caching. +COPY docs/ ./ +RUN npm ci + +# Increase max-old-space-size to avoid memory issues during build +ENV NODE_OPTIONS="--max-old-space-size=8192" + +# Build the site. +RUN npm run docs:build + +# ---------------------- +# 2. Runtime stage (nginx) +# ---------------------- +FROM nginx:1.27-alpine AS runner + +# Copy built files +COPY --from=builder /app/docs/.vitepress/dist /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +# Health check (optional) +HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docs.Dockerfile b/docs.Dockerfile deleted file mode 100644 index 8d5a6570e..000000000 --- a/docs.Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Generate the HTML output. -FROM ghcr.io/cakephp/docs-builder as builder - -COPY docs /data/docs - -RUN cd /data/docs-builder && \ - # In the future repeat website for each version - make website LANGS="en" SOURCE=/data/docs DEST=/data/website/ - -# Build a small nginx container with just the static site in it. -FROM ghcr.io/cakephp/docs-builder:runtime as runtime - -ENV LANGS="en" -ENV SEARCH_SOURCE="/usr/share/nginx/html" -ENV SEARCH_URL_PREFIX="/migrations/5" - -COPY --from=builder /data/docs /data/docs -COPY --from=builder /data/website /data/website -COPY --from=builder /data/docs-builder/nginx.conf /etc/nginx/conf.d/default.conf - - -# Move each version into place -RUN cp -R /data/website/html/* /usr/share/nginx/html \ - && rm -rf /data/website/ - -RUN ln -s /usr/share/nginx/html /usr/share/nginx/html/2.x diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..a1a455b7d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules +*/public/ +.vitepress/cache diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js new file mode 100644 index 000000000..b0ede705f --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,51 @@ +import baseConfig from '@cakephp/docs-skeleton/config' + +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const toc_en = require("./toc_en.json"); + +const versions = { + text: "5.x", + items: [ + { text: "5.x (current)", link: "book.cakephp.org/migrations/5.x/", target: '_self' }, + { text: "4.x", link: "https://book.cakephp.org/migrations/4/", target: '_self' }, + { text: "3.x", link: "https://book.cakephp.org/migrations/3/", target: '_self' }, + { text: "2.x", link: "https://book.cakephp.org/migrations/2/", target: '_self' }, + ], +}; + +// This file contains overrides for .vitepress/config.js +export default { + extends: baseConfig, + srcDir: 'en', + title: 'Migrations plugin', + description: 'Migrations - CakePHP migrations Documentation', + base: "/5.x/", + rewrites: { + "en/:slug*": ":slug*", + }, + sitemap: { + hostname: "https://book.cakephp.org/migrations/5.x/", + }, + themeConfig: { + socialLinks: [ + { icon: "github", link: "https://github.com/cakephp/cakephp" }, + ], + editLink: { + pattern: "https://github.com/cakephp/migrations/edit/5.x/docs/:path", + text: "Edit this page on GitHub", + }, + sidebar: toc_en, + nav: [ + { text: "CakePHP Book", link: "https://book.cakephp.org/" }, + { ...versions }, + ], + }, + substitutions: {}, + locales: { + root: { + label: "English", + lang: "en", + }, + }, +}; diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js new file mode 100644 index 000000000..e33e19ec9 --- /dev/null +++ b/docs/.vitepress/theme/index.js @@ -0,0 +1 @@ +export { default } from '@cakephp/docs-skeleton' diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json new file mode 100644 index 000000000..b7f94176a --- /dev/null +++ b/docs/.vitepress/toc_en.json @@ -0,0 +1,23 @@ +{ + "/": [ + { + "text": "Migrations", + "collapsed": false, + "items": [ + { "text": "Introduction", "link": "/index" }, + { "text": "Writing Migrations", "link": "/writing-migrations" }, + { "text": "Using the Query Builder", "link": "/using-the-query-builder" }, + { "text": "Executing Queries", "link": "/executing-queries" }, + { "text": "Database Seeding", "link": "/seeding" } + ] + }, + { + "text": "Upgrade Guides", + "collapsed": true, + "items": [ + { "text": "Upgrading from 4.x to 5.x", "link": "/upgrading" }, + { "text": "Upgrading to the Builtin Backend", "link": "/upgrading-to-builtin-backend" } + ] + } + ] +} diff --git a/docs/config/__init__.py b/docs/config/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/config/all.py b/docs/config/all.py deleted file mode 100644 index a8f5f2f86..000000000 --- a/docs/config/all.py +++ /dev/null @@ -1,53 +0,0 @@ -# Global configuration information used across all the -# translations of documentation. -# -# Import the base theme configuration -from cakephpsphinx.config.all import * - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# - -# The full version, including alpha/beta/rc tags. -release = '5.x' - -# The search index version. -search_version = 'migrations-5' - -# The marketing display name for the book. -version_name = '' - -# Project name shown in the black header bar -project = 'CakePHP Migrations' - -# Other versions that display in the version picker menu. -version_list = [ - {'name': '2.x', 'number': 'migrations/2', 'title': '2.x'}, - {'name': '3.x', 'number': 'migrations/3', 'title': '3.x'}, - {'name': '4.x', 'number': 'migrations/4', 'title': '4.x'}, - {'name': '5.x', 'number': 'migrations/5', 'title': '5.x', 'current': True}, -] - -# Languages available. -languages = ['en', 'fr', 'ja', 'pt', 'ru'] - -# The GitHub branch name for this version of the docs -# for edit links to point at. -branch = '5.x' - -# Current version being built -version = '5.x' - -# Language in use for this directory. -language = 'en' - -show_root_link = True - -repository = 'cakephp/migrations' - -source_path = 'docs/' - -hide_page_contents = ('search', '404', 'contents') - -is_prerelease = False diff --git a/docs/en/conf.py b/docs/en/conf.py deleted file mode 100644 index f638bda22..000000000 --- a/docs/en/conf.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys, os - -# Append the top level directory of the docs, so we can import from the config dir. -sys.path.insert(0, os.path.abspath('..')) - -# Pull in all the configuration options defined in the global config file.. -from config.all import * - -language = 'en' diff --git a/docs/en/contents.md b/docs/en/contents.md new file mode 100644 index 000000000..18fe41d4c --- /dev/null +++ b/docs/en/contents.md @@ -0,0 +1,8 @@ +### CakePHP Migrations + +- [Migrations 5.x](index) +- [Writing Migrations](writing-migrations) +- [Using the Query Builder](using-the-query-builder) +- [Executing Queries](executing-queries) +- [Database Seeding](seeding) +- [Upgrading to the builtin backend](upgrading-to-builtin-backend) diff --git a/docs/en/contents.rst b/docs/en/contents.rst deleted file mode 100644 index 88edb891a..000000000 --- a/docs/en/contents.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. toctree:: - :maxdepth: 2 - :caption: CakePHP Migrations - - /index - /writing-migrations - /using-the-query-builder - /executing-queries - /seeding - /upgrading-to-builtin-backend diff --git a/docs/en/executing-queries.md b/docs/en/executing-queries.md new file mode 100644 index 000000000..a87c58dd0 --- /dev/null +++ b/docs/en/executing-queries.md @@ -0,0 +1,135 @@ +# Executing Queries + +Queries can be executed with the `execute()` and `query()` methods. The +`execute()` method returns the number of affected rows whereas the +`query()` method returns the result as a +[CakePHP Statement](https://book.cakephp.org/5/en/orm/database-basics.html#interacting-with-statements). Both methods +accept an optional second parameter `$params` which is an array of elements, +and if used will cause the underlying connection to use a prepared statement: + +``` php +execute('DELETE FROM users'); // returns the number of affected rows + + // query() + $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement + $rows = $stmt->fetchAll(); // returns the result as an array + + // using prepared queries + $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); + $stmt = $this->query('SELECT * FROM users WHERE id > ?', [5]); // returns PDOStatement + $rows = $stmt->fetchAll(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Fetching Rows + +There are two methods available to fetch rows. The `fetchRow()` method will +fetch a single row, whilst the `fetchAll()` method will return multiple rows. +Both methods accept raw SQL as their only parameter: + +``` php +fetchRow('SELECT * FROM users'); + + // fetch an array of messages + $rows = $this->fetchAll('SELECT * FROM messages'); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Inserting Data + +Migrations makes it easy to insert data into your tables. Whilst this feature is +intended for the [seed feature](seeding), you are also free to use the +insert methods in your migrations: + +``` php +table('status'); + + // inserting only one row + $singleRow = [ + 'id' => 1, + 'name' => 'In Progress' + ]; + + $table->insert($singleRow)->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped' + ], + [ + 'id' => 3, + 'name' => 'Queued' + ] + ]; + + $table->insert($rows)->saveData(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $this->execute('DELETE FROM status'); + } +} +``` + +> [!NOTE] +> You cannot use the insert methods inside a change() method. Please use the +> up() and down() methods. diff --git a/docs/en/executing-queries.rst b/docs/en/executing-queries.rst deleted file mode 100644 index de26a5490..000000000 --- a/docs/en/executing-queries.rst +++ /dev/null @@ -1,134 +0,0 @@ -Executing Queries -################# - -Queries can be executed with the ``execute()`` and ``query()`` methods. The -``execute()`` method returns the number of affected rows whereas the -``query()`` method returns the result as a -`CakePHP Statement `_. Both methods -accept an optional second parameter ``$params`` which is an array of elements, -and if used will cause the underlying connection to use a prepared statement:: - - execute('DELETE FROM users'); // returns the number of affected rows - - // query() - $stmt = $this->query('SELECT * FROM users'); // returns PDOStatement - $rows = $stmt->fetchAll(); // returns the result as an array - - // using prepared queries - $count = $this->execute('DELETE FROM users WHERE id = ?', [5]); - $stmt = $this->query('SELECT * FROM users WHERE id > ?', [5]); // returns PDOStatement - $rows = $stmt->fetchAll(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Fetching Rows -============= - -There are two methods available to fetch rows. The ``fetchRow()`` method will -fetch a single row, whilst the ``fetchAll()`` method will return multiple rows. -Both methods accept raw SQL as their only parameter:: - - fetchRow('SELECT * FROM users'); - - // fetch an array of messages - $rows = $this->fetchAll('SELECT * FROM messages'); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Inserting Data -============== - -Migrations makes it easy to insert data into your tables. Whilst this feature is -intended for the :doc:`seed feature `, you are also free to use the -insert methods in your migrations:: - - table('status'); - - // inserting only one row - $singleRow = [ - 'id' => 1, - 'name' => 'In Progress' - ]; - - $table->insert($singleRow)->saveData(); - - // inserting multiple rows - $rows = [ - [ - 'id' => 2, - 'name' => 'Stopped' - ], - [ - 'id' => 3, - 'name' => 'Queued' - ] - ]; - - $table->insert($rows)->saveData(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $this->execute('DELETE FROM status'); - } - } - -.. note:: - - You cannot use the insert methods inside a `change()` method. Please use the - `up()` and `down()` methods. - diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 000000000..6eb56c665 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,941 @@ +# Migrations 5.x + +Migrations is a plugin that lets you track changes to your database schema over +time as PHP code that accompanies your application. This lets you ensure each +environment your application runs in has the appropriate schema by applying +migrations. + +Instead of writing schema modifications in SQL, this plugin allows you to +define schema changes with a high-level database portable API. + +## What's New in 5.x + +Migrations 5.x includes several new features: + +- **Seed tracking** - Seeds are now tracked in a `cake_seeds` table, preventing + accidental re-runs +- **Check constraints** - Support for database check constraints via `addCheckConstraint()` +- **Default values in bake** - Specify default values when baking migrations + (e.g., `active:boolean:default[true]`) +- **MySQL ALTER options** - Control `ALGORITHM` and `LOCK` for ALTER TABLE operations +- **insertOrSkip()** - New method for idempotent seed data insertion + +See the [Upgrading from 4.x to 5.x](upgrading) guide for breaking changes and migration steps from 4.x. + +## Installation + +By default Migrations is installed with the application skeleton. If +you've removed it and want to re-install it, you can do so by running the +following from your application's ROOT directory (where **composer.json** file is +located): + +``` bash +php composer.phar require cakephp/migrations "@stable" + +# Or if composer is installed globally +composer require cakephp/migrations "@stable" +``` + +To use the plugin you'll need to load it in your application's +**config/bootstrap.php** file. You can use [CakePHP's Plugin shell](https://book.cakephp.org/5/en/console-and-shells/plugin-shell.html) to +load and unload plugins from your **config/bootstrap.php**: + +``` bash +bin/cake plugin load Migrations +``` + +Or you can load the plugin by editing your **src/Application.php** file and +adding the following statement: + +``` php +$this->addPlugin('Migrations'); +``` + +Additionally, you will need to configure the default database configuration for +your application in your **config/app.php** file as explained in the [Database +Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration). + +## Upgrading from 4.x + +If you are upgrading from Migrations 4.x, please see the [Upgrading from 4.x to 5.x](upgrading) guide +for breaking changes and migration steps. + +## Overview + +A migration is a PHP file that describes the changes to apply to your database. +A migration file can add, change or remove tables, columns, indexes and foreign keys. + +If we wanted to create a table, we could use a migration similar to this: + +``` php +table('products'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('description', 'text', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->create(); + } +} +``` + +When applied, this migration will add a table to your database named +`products` with the following column definitions: + +- `id` column of type `integer` as primary key. This column is added + implicitly, but you can customize the name and type if necessary. +- `name` column of type `string` +- `description` column of type `text` +- `created` column of type `datetime` +- `modified` column of type `datetime` + +> [!NOTE] +> Migrations are not automatically applied, you can apply and rollback +> migrations with CLI commands. + +Once the file has been created in the **config/Migrations** folder, you can +apply it: + +``` bash +bin/cake migrations migrate +``` + +## Creating Migrations + +Migration files are stored in the **config/Migrations** directory of your +application. The name of the migration files are prefixed with the date in +which they were created, in the format **YYYYMMDDHHMMSS_MigrationName.php**. +Here are examples of migration filenames: + +- **20160121163850_CreateProducts.php** +- **20160210133047_AddRatingToProducts.php** + +The easiest way to create a migrations file is by using `bin/cake bake migration` CLI command: + +``` bash +bin/cake bake migration CreateProducts +``` + +This will create an empty migration that you can edit to add any columns, +indexes and foreign keys you need. See the [Creating A Table](writing-migrations#creating-a-table) section to +learn more about using migrations to define tables. + +> [!NOTE] +> Migrations need to be applied using `bin/cake migrations migrate` after +> they have been created. + +### Migration file names + +When generating a migration, you can follow one of the following patterns +to have additional skeleton code generated: + +- `/^(Create)(.*)/` Creates the specified table. +- `/^(Drop)(.*)/` Drops the specified table. + Ignores specified field arguments +- `/^(Add).*(?:To)(.*)/` Adds fields to the specified + table +- `/^(Remove).*(?:From)(.*)/` Removes fields from the + specified table +- `/^(Alter)(.*)/` Alters the specified table. An alias + for CreateTable and AddField. +- `/^(Alter).*(?:On)(.*)/` Alters fields from the specified table. + +You can also use the `underscore_form` as the name for your migrations i.e. +`create_products`. + +> [!WARNING] +> Migration names are used as class names, and thus may collide with +> other migrations if the class names are not unique. In this case, it may be +> necessary to manually override the name at a later date, or simply change +> the name you are specifying. + +### Creating a table + +You can use `bake migration` to create a table: + +``` bash +bin/cake bake migration CreateProducts name:string description:text created modified +``` + +The command line above will generate a migration file that resembles: + +``` php +table('products'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('description', 'text', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->create(); + } +} +``` + +### Column syntax + +The `bake migration` command provides a compact syntax to define columns when +generating a migration: + +``` bash +bin/cake bake migration CreateProducts name:string description:text created modified +``` + +You can use the column syntax when creating tables and adding columns. You can +also edit the migration after generation to add or customize the columns + +Columns on the command line follow the following pattern: + + fieldName:fieldType?[length]:default[value]:indexType:indexName + +For instance, the following are all valid ways of specifying an email field: + +- `email:string?` +- `email:string:unique` +- `email:string?[50]` +- `email:string:unique:EMAIL_INDEX` +- `email:string[120]:unique:EMAIL_INDEX` + +While defining decimal columns, the `length` can be defined to have precision +and scale, separated by a comma. + +- `amount:decimal[5,2]` +- `amount:decimal?[5,2]` + +Columns with a question mark after the fieldType will make the column nullable. + +The `length` part is optional and should always be written between bracket. + +The `default[value]` part is optional and sets the default value for the column. +Supported value types include: + +- Booleans: `true` or `false` - e.g., `active:boolean:default[true]` +- Integers: `0`, `123`, `-456` - e.g., `count:integer:default[0]` +- Floats: `1.5`, `-2.75` - e.g., `rate:decimal:default[1.5]` +- Strings: `'hello'` or `"world"` (quoted) - e.g., `status:string:default['pending']` +- Null: `null` or `NULL` - e.g., `description:text?:default[null]` +- SQL expressions: `CURRENT_TIMESTAMP` - e.g., `created_at:datetime:default[CURRENT_TIMESTAMP]` + +Fields named `created` and `modified`, as well as any field with a `_at` +suffix, will automatically be set to the type `datetime`. + +There are some heuristics to choosing fieldtypes when left unspecified or set to +an invalid value. Default field type is `string`: + +- id: integer +- created, modified, updated: datetime +- latitude, longitude (or short forms lat, lng): decimal + +Additionally you can create an empty migrations file if you want full control +over what needs to be executed, by omitting to specify a columns definition: + +``` bash +bin/cake migrations create MyCustomMigration +``` + +See [Writing Migrations](writing-migrations) for more information on how to use `Table` +objects to interact with tables and define schema changes. + +### Adding columns to an existing table + +If the migration name in the command line is of the form "AddXXXToYYY" and is +followed by a list of column names and types then a migration file containing +the code for creating the columns will be generated: + +``` bash +bin/cake bake migration AddPriceToProducts price:decimal[5,2] +``` + +Executing the command line above will generate: + +``` php +table('products'); + $table->addColumn('price', 'decimal', [ + 'default' => null, + 'null' => false, + 'precision' => 5, + 'scale' => 2, + ]); + $table->update(); + } +} +``` + +### Adding a column with an index + +It is also possible to add indexes to columns: + +``` bash +bin/cake bake migration AddNameIndexToProducts name:string:index +``` + +will generate: + +``` php +table('products'); + $table->addColumn('name', 'string') + ->addColumn('email', 'string') + ->addIndex(['name']) + // add a unique index: + ->addIndex('email', ['unique' => true]) + ->update(); + } +} +``` + +### Adding a column with a default value + +You can specify default values for columns using the `default[value]` syntax: + +``` bash +bin/cake bake migration AddActiveToUsers active:boolean:default[true] +``` + +will generate: + +``` php +table('users'); + $table->addColumn('active', 'boolean', [ + 'default' => true, + 'null' => false, + ]); + $table->update(); + } +} +``` + +You can combine default values with other options like nullable and indexes: + +``` bash +bin/cake bake migration AddStatusToOrders status:string:default['pending']:unique +``` + +### Altering a column + +In the same way, you can generate a migration to alter a column by using the +command line, if the migration name is of the form "AlterXXXOnYYY": + +``` bash +bin/cake bake migration AlterPriceOnProducts name:float +``` + +will generate: + +``` php +table('products'); + $table->changeColumn('name', 'float'); + $table->update(); + } +} +``` + +> [!WARNING] +> Changing the type of a column can result in data loss if the +> current and target column type are not compatible. For example converting +> a varchar to float. + +### Removing a column + +In the same way, you can generate a migration to remove a column by using the +command line, if the migration name is of the form "RemoveXXXFromYYY": + +``` bash +bin/cake bake migration RemovePriceFromProducts price +``` + +creates the file: + +``` php +table('products'); + $table->removeColumn('price') + ->save(); + } +} +``` + +> [!NOTE] +> The removeColumn command is not reversible, so must be called in the +> up method. A corresponding addColumn call should be added to the +> down method. + +## Generating migration snapshots from an existing database + +If you have a pre-existing database and want to start using +migrations, or to version control the initial schema of your application's +database, you can run the `bake migration_snapshot` command: + +``` bash +bin/cake bake migration_snapshot Initial +``` + +It will generate a migration file called **YYYYMMDDHHMMSS_Initial.php** +containing all the create statements for all tables in your database. + +By default, the snapshot will be created by connecting to the database defined +in the `default` connection configuration. If you need to bake a snapshot from +a different datasource, you can use the `--connection` option: + +``` bash +bin/cake bake migration_snapshot Initial --connection my_other_connection +``` + +You can also make sure the snapshot includes only the tables for which you have +defined the corresponding model classes by using the `--require-table` flag: + +``` bash +bin/cake bake migration_snapshot Initial --require-table +``` + +When using the `--require-table` flag, the shell will look through your +application `Table` classes and will only add the model tables in the snapshot. + +If you want to generate a snapshot without marking it as migrated (for example, +for use in unit tests), you can use the `--generate-only` flag: + +``` bash +bin/cake bake migration_snapshot Initial --generate-only +``` + +This will create the migration file but will not add an entry to the migrations +tracking table, allowing you to move the file to a different location without causing +"MISSING" status issues. + +The same logic will be applied implicitly if you wish to bake a snapshot for a +plugin. To do so, you need to use the `--plugin` option: + +``` bash +bin/cake bake migration_snapshot Initial --plugin MyPlugin +``` + +Only the tables which have a `Table` object model class defined will be added +to the snapshot of your plugin. + +> [!NOTE] +> When baking a snapshot for a plugin, the migration files will be created +> in your plugin's **config/Migrations** directory. + +Be aware that when you bake a snapshot, it is automatically added to the +migrations log table as migrated. + +## Generating a diff + +As migrations are applied and rolled back, the migrations plugin will generate +a 'dump' file of your schema. If you make manual changes to your database schema +outside of migrations, you can use `bake migration_diff` to generate +a migration file that captures the difference between the current schema dump +file and database schema. To do so, you can use the following command: + +``` bash +bin/cake bake migration_diff NameOfTheMigrations +``` + +By default, the diff will be created by connecting to the database defined +in the `default` connection configuration. +If you need to bake a diff from a different datasource, you can use the +`--connection` option: + +``` bash +bin/cake bake migration_diff NameOfTheMigrations --connection my_other_connection +``` + +If you want to use the diff feature on an application that already has a +migrations history, you need to manually create the dump file that will be used +as comparison: + +``` bash +bin/cake migrations dump +``` + +The database state must be the same as it would be if you just migrated all +your migrations before you create a dump file. +Once the dump file is generated, you can start doing changes in your database +and use the `bake migration_diff` command whenever you see fit. + +> [!NOTE] +> Migration diff generation can not detect column renamings. + +## Applying Migrations + +Once you have generated or written your migration file, you need to execute the +following command to apply the changes to your database: + +``` bash +# Run all the migrations +bin/cake migrations migrate + +# Migrate to a specific version using the ``--target`` option +# or ``-t`` for short. +# The value is the timestamp that is prefixed to the migrations file name:: +bin/cake migrations migrate -t 20150103081132 + +# By default, migration files are looked for in the **config/Migrations** +# directory. You can specify the directory using the ``--source`` option +# or ``-s`` for short. +# The following example will run migrations in the **config/Alternate** +# directory +bin/cake migrations migrate -s Alternate + +# You can run migrations to a different connection than the ``default`` one +# using the ``--connection`` option or ``-c`` for short +bin/cake migrations migrate -c my_custom_connection + +# Migrations can also be run for plugins. Simply use the ``--plugin`` option +# or ``-p`` for short +bin/cake migrations migrate -p MyAwesomePlugin +``` + +## Reverting Migrations + +The rollback command is used to undo previous migrations executed by this +plugin. It is the reverse action of the `migrate` command: + +``` bash +# You can rollback to the previous migration by using the +# ``rollback`` command:: +bin/cake migrations rollback + +# You can also pass a migration version number to rollback +# to a specific version:: +bin/cake migrations rollback -t 20150103081132 +``` + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +## View Migrations Status + +The Status command prints a list of all migrations, along with their current +status. You can use this command to determine which migrations have been run: + +``` bash +bin/cake migrations status +``` + +You can also output the results as a JSON formatted string using the +`--format` option (or `-f` for short): + +``` bash +bin/cake migrations status --format json +``` + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +### Cleaning up missing migrations + +Sometimes migration files may be deleted from the filesystem but still exist +in the migrations tracking table. These migrations will be marked as **MISSING** in the +status output. You can remove these entries from the tracking table using the +`--cleanup` option: + +``` bash +bin/cake migrations status --cleanup +``` + +This will remove all migration entries from the tracking table that no longer +have corresponding migration files in the filesystem. + +## Marking a migration as migrated + +It can sometimes be useful to mark a set of migrations as migrated without +actually running them. In order to do this, you can use the `mark_migrated` +command. The command works seamlessly as the other commands. + +You can mark all migrations as migrated using this command: + +``` bash +bin/cake migrations mark_migrated +``` + +You can also mark all migrations up to a specific version as migrated using +the `--target` option: + +``` bash +bin/cake migrations mark_migrated --target=20151016204000 +``` + +If you do not want the targeted migration to be marked as migrated during the +process, you can use the `--exclude` flag with it: + +``` bash +bin/cake migrations mark_migrated --target=20151016204000 --exclude +``` + +Finally, if you wish to mark only the targeted migration as migrated, you can +use the `--only` flag: + +``` bash +bin/cake migrations mark_migrated --target=20151016204000 --only +``` + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +> [!NOTE] +> When you bake a snapshot with the `cake bake migration_snapshot` +> command, the created migration will automatically be marked as migrated. +> To prevent this behavior (e.g., for unit test migrations), use the +> `--generate-only` flag. + +This command expects the migration version number as argument: + +``` bash +bin/cake migrations mark_migrated 20150420082532 +``` + +If you wish to mark all migrations as migrated, you can use the `all` special +value. If you use it, it will mark all found migrations as migrated: + +``` bash +bin/cake migrations mark_migrated all +``` + +## Seeding your database + +Seed classes are a good way to populate your database with default or starter +data. They are also a great way to generate data for development environments. + +By default, seeds will be looked for in the `config/Seeds/` directory of +your application. See the [Database Seeding](seeding) for how to build and use seed classes. + +## Generating a dump file + +The dump command creates a file to be used with the `bake migration_diff` +command: + +``` bash +bin/cake migrations dump +``` + +Each generated dump file is specific to the Connection it is generated from (and +is suffixed as such). This allows the `bake migration_diff` command to +properly compute diff in case your application is dealing with multiple database +possibly from different database vendors. + +Dump files are created in the same directory as your migrations files. + +You can also use the `--source`, `--connection` and `--plugin` options +just like for the `migrate` command. + +## Using Migrations for Tests + +If you are using migrations for your application schema you can also use those +same migrations to build schema in your tests. In your application's +`tests/bootstrap.php` file you can use the `Migrator` class to build schema +when tests are run. The `Migrator` will use existing schema if it is current, +and if the migration history that is in the database differs from what is in the +filesystem, all tables will be dropped and migrations will be rerun from the +beginning: + +``` php +// in tests/bootstrap.php +use Migrations\TestSuite\Migrator; + +$migrator = new Migrator(); + +// Simple setup for with no plugins +$migrator->run(); + +// Run a non 'test' database +$migrator->run(['connection' => 'test_other']); + +// Run migrations for plugins +$migrator->run(['plugin' => 'Contacts']); + +// Run the Documents migrations on the test_docs connection. +$migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']); +``` + +If you need to run multiple sets of migrations, those can be run as follows: + +``` php +// Run migrations for plugin Contacts on the ``test`` connection, and Documents on the ``test_docs`` connection +$migrator->runMany([ + ['plugin' => 'Contacts'], + ['plugin' => 'Documents', 'connection' => 'test_docs'] +]); +``` + +If your database also contains tables that are not managed by your application +like those created by PostGIS, then you can exclude those tables from the drop +& truncate behavior using the `skip` option: + +``` php +$migrator->run(['connection' => 'test', 'skip' => ['postgis*']]); +``` + +The `skip` option accepts a `fnmatch()` compatible pattern to exclude tables +from drop & truncate operations. + +If you need to see additional debugging output from migrations are being run, +you can enable a `debug` level logger. + +## Using Migrations In Plugins + +Plugins can also provide migration files. This makes plugins that are intended +to be distributed much more portable and easy to install. All commands in the +Migrations plugin support the `--plugin` or `-p` option that will scope the +execution to the migrations relative to that plugin: + +``` bash +bin/cake migrations status -p PluginName + +bin/cake migrations migrate -p PluginName +``` + +## Running Migrations in a non-shell environment + +While typical usage of migrations is from the command line, you can also run +migrations from a non-shell environment, by using +`Migrations\Migrations` class. This can be handy in case you are developing a plugin +installer for a CMS for instance. The `Migrations` class allows you to run the +following commands from the migrations shell: + +- migrate +- rollback +- markMigrated +- status +- seed + +Each of these commands has a method defined in the `Migrations` class. + +Here is how to use it: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(); + +// Will return an array of all migrations and their status +$status = $migrations->status(); + +// Will return true if success. If an error occurred, an exception will be thrown +$migrate = $migrations->migrate(); + +// Will return true if success. If an error occurred, an exception will be thrown +$rollback = $migrations->rollback(); + +// Will return true if success. If an error occurred, an exception will be thrown +$markMigrated = $migrations->markMigrated(20150804222900); + +// Will return true if success. If an error occurred, an exception will be thrown +$seeded = $migrations->seed(); +``` + +The methods can accept an array of parameters that should match options from +the commands: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(); + +// Will return an array of all migrations and their status +$status = $migrations->status(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); +``` + +You can pass any options the shell commands would take. +The only exception is the `markMigrated` command which is expecting the +version number of the migrations to mark as migrated as first argument. Pass +the array of parameters as the second argument for this method. + +Optionally, you can pass these parameters in the constructor of the class. +They will be used as default and this will prevent you from having to pass +them on each method call: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); + +// All the following calls will be done with the parameters passed to the Migrations class constructor +$status = $migrations->status(); +$migrate = $migrations->migrate(); +``` + +If you need to override one or more default parameters for one call, you can +pass them to the method: + +``` php +use Migrations\Migrations; + +$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); + +// This call will be made with the "custom" connection +$status = $migrations->status(); +// This one with the "default" connection +$migrate = $migrations->migrate(['connection' => 'default']); +``` + + + +## Feature Flags + +Migrations offers a few feature flags for compatibility. These features are disabled by default but can be enabled if required: + +- `unsigned_primary_keys`: Should Migrations create primary keys as unsigned integers? (default: `false`) +- `unsigned_ints`: Should Migrations create all integer columns as unsigned? (default: `false`) +- `column_null_default`: Should Migrations create columns as null by default? (default: `false`) +- `add_timestamps_use_datetime`: Should Migrations use `DATETIME` type + columns for the columns added by `addTimestamps()`. + +Set them via Configure to enable (e.g. in `config/app.php`): + +``` text +'Migrations' => [ + 'unsigned_primary_keys' => true, + 'unsigned_ints' => true, + 'column_null_default' => true, +], +``` + +> [!NOTE] +> The `unsigned_primary_keys` and `unsigned_ints` options only affect MySQL databases. +> When generating migrations with `bake migration_snapshot` or `bake migration_diff`, +> the `signed` attribute will only be included in the output for unsigned columns +> (as `'signed' => false`). Signed is the default for integer columns in MySQL, so +> `'signed' => true` is never output. + +## Skipping the `schema.lock` file generation + +In order for the diff feature to work, a **.lock** file is generated everytime +you migrate, rollback or bake a snapshot, to keep track of the state of your +database schema at any given point in time. You can skip this file generation, +for instance when deploying on your production environment, by using the +`--no-lock` option for the aforementioned command: + +``` bash +bin/cake migrations migrate --no-lock + +bin/cake migrations rollback --no-lock + +bin/cake bake migration_snapshot MyMigration --no-lock +``` + +## Deployment + +You should update your deployment scripts to run migrations when new code is +deployed. Ideally you want to run migrations after the code is on your servers, +but before the application code becomes active. + +After running migrations remember to clear the ORM cache so it renews the column +metadata of your tables. Otherwise, you might end up having errors about +columns not existing when performing operations on those new columns. The +CakePHP Core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you +can use to perform this operation: + +``` bash +bin/cake migration migrate +bin/cake schema_cache clear +``` + +## Alert of missing migrations + +You can use the `Migrations.PendingMigrations` middleware in local development +to alert developers about new migrations that have not been applied: + +``` php +use Migrations\Middleware\PendingMigrationsMiddleware; + +$config = [ + 'plugins' => [ + ... // Optionally include a list of plugins with migrations to check. + ], +]; + +$middlewareQueue + ... // ErrorHandler middleware + ->add(new PendingMigrationsMiddleware($config)) + ... // rest +``` + +You can add `'app'` config key set to `false` if you are only interested in +checking plugin migrations. + +You can temporarily disable the migration check by adding +`skip-migration-check=1` to the URL query string + +## IDE autocomplete support + +The [IdeHelper plugin](https://github.com/dereuromark/cakephp-ide-helper) can help +you to get more IDE support for the tables, their column names and possible column types. +Specifically PHPStorm understands the meta information and can help you autocomplete those. diff --git a/docs/en/seeding.md b/docs/en/seeding.md new file mode 100644 index 000000000..c2d21c501 --- /dev/null +++ b/docs/en/seeding.md @@ -0,0 +1,585 @@ +# Database Seeding + +Seed classes are a great way to easily fill your database with data after +it's created. By default, they are stored in the `config/Seeds` directory. + +> [!NOTE] +> Database seeding is entirely optional, and Migrations does not create a Seeds +> directory by default. + +## Creating a New Seed Class + +Migrations includes a command to easily generate a new seed class: + +``` bash +$ bin/cake bake seed MyNewSeed +``` + +By default, it generates a traditional seed class with a named class: + +``` php +call('Articles');` or `$this->call('ArticlesSeed');` + +Using the short name is recommended for cleaner, more concise code. + +### Anonymous Seed Classes + +Migrations also supports generating anonymous seed classes, which use PHP's +anonymous class feature instead of named classes. This style is useful for: + +- Avoiding namespace declarations +- Better PHPCS compatibility (no class name to filename matching required) +- Simpler file structure without named class constraints + +To generate an anonymous seed class, use the `--style anonymous` option: + +``` bash +$ bin/cake bake seed MyNewSeed --style anonymous +``` + +This generates a seed file using an anonymous class: + +``` php + [ + 'style' => 'anonymous', // or 'traditional' +], +``` + +### Seed Options + +``` bash +# you specify the name of the table the seed files will alter by using the ``--table`` option +bin/cake bake seed articles --table my_articles_table +# You can specify a plugin to bake into +bin/cake bake seed Articles --plugin PluginName + +# You can specify an alternative connection when generating a seed. +bin/cake bake seed Articles --connection connection + +# Include data from the Articles table in your seed. +bin/cake bake seed --data Articles +``` + +By default, it will export all the rows found in your table. You can limit the +number of rows exported by using the `--limit` option: + +``` bash +# Will only export the first 10 rows found +bin/cake bake seed --data --limit 10 Articles +``` + +If you only want to include a selection of fields from the table in your seed +file, you can use the `--fields` option. It takes the list of fields to +include as a comma separated value string: + +``` bash +# Will only export the fields `id`, `title` and `excerpt` +bin/cake bake seed --data --fields id,title,excerpt Articles +``` + +> [!TIP] +> Of course you can use both the `--limit` and `--fields` options in the +> same command call. + + + +### Customizing Seed and Migration templates + +Because migrations uses [bake](https://book.cakephp.org/bake) under the hood +you can customize the templates that migrations uses for creating seeds and +migrations by creating templates in your application. Custom templates for +migrations should be on one of the following paths: + +- `ROOT/templates/plugin/Migrations/bake/` +- `ROOT/templates/bake/` + +For example, the seed templates are: + +- Traditional: `Seed/seed.twig` at **ROOT/templates/plugin/Migrations/bake/Seed/seed.twig** +- Anonymous: `Seed/seed-anonymous.twig` at **ROOT/templates/plugin/Migrations/bake/Seed/seed-anonymous.twig** + +## The BaseSeed Class + +All Migrations seeds extend from the `BaseSeed` class. +It provides the necessary support to create your seed classes. Seed +classes are primarily used to insert test data. + +## The Run Method + +The run method is automatically invoked by Migrations when you execute the +`cake seeds run` command. You should use this method to insert your test +data. + +## Seed Execution Tracking + +Seeds track their execution state in the `cake_seeds` database table. By default, +a seed will only run once. If you attempt to run a seed that has already been +executed, it will be skipped with an "already executed" message. + +To re-run a seed that has already been executed, use the `--force` flag: + +``` bash +bin/cake seeds run Users --force +``` + +You can check which seeds have been executed using the status command: + +``` bash +bin/cake seeds status +``` + +To reset all seeds' execution state (allowing them to run again without `--force`): + +``` bash +bin/cake seeds reset +``` + +> [!NOTE] +> When re-running seeds with `--force`, be careful to ensure your seeds are +> idempotent (safe to run multiple times) or they may create duplicate data. + +### Customizing the Seed Tracking Table + +By default, seed execution is tracked in a table named `cake_seeds`. You can +customize this table name by configuring it in your `config/app.php` or +`config/app_local.php`: + +``` php +'Migrations' => [ + 'seed_table' => 'my_custom_seeds_table', +], +``` + +This is useful if you need to avoid table name conflicts or want to follow +a specific naming convention in your database. + +## Idempotent Seeds + +Some seeds are designed to be run multiple times safely (idempotent), such as seeds +that update configuration or reference data. For these seeds, you can override the +`isIdempotent()` method: + +``` php +execute(" + INSERT INTO settings (setting_key, setting_value) + VALUES ('app_version', '2.0.0') + ON DUPLICATE KEY UPDATE setting_value = '2.0.0' + "); + + // Or check before inserting + $exists = $this->fetchRow( + "SELECT COUNT(*) as count FROM settings WHERE setting_key = 'maintenance_mode'" + ); + + if ($exists['count'] == 0) { + $this->table('settings')->insert([ + 'setting_key' => 'maintenance_mode', + 'setting_value' => 'false', + ])->save(); + } + } +} +``` + +When `isIdempotent()` returns `true`: + +- The seed will run **every time** you execute `seeds run` +- The last execution time is still tracked in the `cake_seeds` table +- The `seeds status` command will show the seed as `(idempotent)` +- You must ensure the seed's `run()` method handles duplicate executions safely + +This is useful for: + +- Configuration seeds that should always reflect current values +- Reference data that may need periodic updates +- Seeds that use `INSERT ... ON DUPLICATE KEY UPDATE` or similar patterns +- Development/testing seeds that need to run repeatedly + +> [!WARNING] +> Only mark a seed as idempotent if you've verified it's safe to run multiple times. +> Otherwise, you may create duplicate data or other unexpected behavior. + +## The Init Method + +The `init()` method is run by Migrations before the run method if it exists. This +can be used to initialize properties of the Seed class before using run. + +## The Should Execute Method + +The `shouldExecute()` method is run by Migrations before executing the seed. +This can be used to prevent the seed from being executed at this time. It always +returns true by default. You can override it in your custom `BaseSeed` +implementation. + +## Foreign Key Dependencies + +Often you'll find that seeds need to run in a particular order, so they don't +violate foreign key constraints. To define this order, you can implement the +`getDependencies()` method that returns an array of seeds to run before the +current seed: + +``` php + [!NOTE] +> Dependencies that have already been executed (according to the `cake_seeds` +> table) will be skipped, unless you use the `--force` flag which will +> re-execute all seeds including dependencies. + +## Calling a Seed from another Seed + +Usually when seeding, the order in which to insert the data must be respected +to not encounter constraint violations. Since seeds are executed in an +alphabetical order by default, you can use the `\Migrations\BaseSeed::call()` +method to define your own sequence of seeds execution: + +``` php +call('Another'); // Short name without 'Seed' suffix + $this->call('YetAnother'); // Short name without 'Seed' suffix + + // You can use the plugin dot syntax to call seeds from a plugin + $this->call('PluginName.FromPlugin'); + } +} +``` + +You can also use the full seed name including the `Seed` suffix: + +``` php +$this->call('AnotherSeed'); +$this->call('YetAnotherSeed'); +$this->call('PluginName.FromPluginSeed'); +``` + +Both forms are supported and work identically. + +## Inserting Data + +Seed classes can also use the familiar `Table` object to insert data. You can +retrieve an instance of the Table object by calling the `table()` method from +within your seed class and then use the `insert()` method to insert data: + +``` php + 'foo', + 'created' => date('Y-m-d H:i:s'), + ],[ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + } +} +``` + +> [!NOTE] +> You must call the `saveData()` method to commit your data to the table. +> Migrations will buffer data until you do so. + +## Insert Modes + +In addition to the standard `insert()` method, Migrations provides specialized +insert methods for handling conflicts with existing data. + +### Insert or Skip + +The `insertOrSkip()` method inserts rows but silently skips any that would +violate a unique constraint: + +``` php + 'USD', 'name' => 'US Dollar'], + ['code' => 'EUR', 'name' => 'Euro'], + ]; + + $this->table('currencies') + ->insertOrSkip($data) + ->saveData(); + } +} +``` + +### Insert or Update (Upsert) + +The `insertOrUpdate()` method performs an "upsert" operation - inserting new +rows and updating existing rows that conflict on unique columns: + +``` php + 'USD', 'rate' => 1.0000], + ['code' => 'EUR', 'rate' => 0.9234], + ]; + + $this->table('exchange_rates') + ->insertOrUpdate($data, ['rate'], ['code']) + ->saveData(); + } +} +``` + +The method takes three arguments: + +- `$data`: The rows to insert (same format as `insert()`) +- `$updateColumns`: Which columns to update when a conflict occurs +- `$conflictColumns`: Which columns define uniqueness (must have a unique index) + +> [!WARNING] +> Database-specific behavior differences: +> +> **MySQL**: Uses `ON DUPLICATE KEY UPDATE`. The `$conflictColumns` parameter +> is ignored because MySQL automatically applies the update to *all* unique +> constraint violations on the table. Passing `$conflictColumns` will trigger +> a warning. If your table has multiple unique constraints, be aware that a +> conflict on *any* of them will trigger the update. +> +> **PostgreSQL/SQLite**: Uses `ON CONFLICT (...) DO UPDATE SET`. The +> `$conflictColumns` parameter is required and specifies exactly which unique +> constraint should trigger the update. A `RuntimeException` will be thrown +> if this parameter is empty. +> +> **SQL Server**: Not currently supported. Use separate insert/update logic. + +## Truncating Tables + +In addition to inserting data Migrations makes it trivial to empty your tables using the +SQL TRUNCATE command: + +``` php + 'foo', + 'created' => date('Y-m-d H:i:s'), + ], + [ + 'body' => 'bar', + 'created' => date('Y-m-d H:i:s'), + ] + ]; + + $posts = $this->table('posts'); + $posts->insert($data) + ->saveData(); + + // empty the table + $posts->truncate(); + } +} +``` + +> [!NOTE] +> SQLite doesn't natively support the `TRUNCATE` command so behind the scenes +> `DELETE FROM` is used. It is recommended to call the `VACUUM` command +> after truncating a table. Migrations does not do this automatically. + +## Executing Seed Classes + +This is the easy part. To seed your database, simply use the `seeds run` command: + +``` bash +$ bin/cake seeds run +``` + +By default, Migrations will execute all available seed classes. If you would like to +run a specific seed, simply pass in the seed name as an argument. +You can use either the short name (without the `Seed` suffix) or the full name: + +``` bash +$ bin/cake seeds run User +# or +$ bin/cake seeds run UserSeed +``` + +Both commands work identically. + +You can also run multiple seeds by separating them with commas: + +``` bash +$ bin/cake seeds run User,Permission,Log +# or with full names +$ bin/cake seeds run UserSeed,PermissionSeed,LogSeed +``` + +You can also use the -v parameter for more output verbosity: + +``` bash +$ bin/cake seeds run -v +``` + +The Migrations seed functionality provides a simple mechanism to easily and repeatably +insert test data into your database, this is great for development environment +sample data or getting state for demos. diff --git a/docs/en/upgrading-to-builtin-backend.md b/docs/en/upgrading-to-builtin-backend.md new file mode 100644 index 000000000..59a034c1b --- /dev/null +++ b/docs/en/upgrading-to-builtin-backend.md @@ -0,0 +1,187 @@ +# Upgrading to the builtin backend + +As of migrations 4.3 there is a new migrations backend that uses CakePHP's +database abstractions and ORM. In 4.4, the `builtin` backend became the +default backend. As of migrations 5.0, phinx has been removed as a dependency +and only the builtin backend is supported. This greatly reduces the dependency +footprint of migrations. + +## What is the same? + +Your migrations shouldn't have to change much to adapt to the new backend. +The builtin backend provides similar functionality to what was available with +phinx. If your migrations don't work in a way that could be addressed by the +changes outlined below, please open an issue. + +## What is different? + +### Command Structure Changes + +As of migrations 5.0, the command structure has changed. The old phinx wrapper +commands have been removed and replaced with new command names: + +**Seeds:** + +``` bash +# Old (4.x and earlier) +bin/cake migrations seed +bin/cake migrations seed --seed Articles + +# New (5.x and later) +bin/cake seeds run +bin/cake seeds run Articles +``` + +The new commands are: + +- `bin/cake seeds run` - Run seed classes +- `bin/cake seeds status` - Show seed execution status +- `bin/cake seeds reset` - Reset seed execution tracking +- `bin/cake bake seed` - Generate new seed classes + +#### Maintaining Backward Compatibility + +If you need to maintain the old command structure for existing scripts or CI/CD +pipelines, you can add command aliases in your application. In your +`src/Application.php` file, add the following to the `console()` method: + +``` php +public function console(CommandCollection $commands): CommandCollection +{ + // Add your application's commands + $commands = $this->addConsoleCommands($commands); + + // Add backward compatibility aliases for migrations 4.x commands + $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); + + return $commands; +} +``` + +For multiple aliases, you can add them all together: + +``` php +// Add multiple backward compatibility aliases +$commands->add('migrations seed', \Migrations\Command\SeedCommand::class); +$commands->add('migrations seed:run', \Migrations\Command\SeedCommand::class); +$commands->add('migrations seed:status', \Migrations\Command\SeedStatusCommand::class); +``` + +This allows gradual migration of scripts and documentation without modifying the +migrations plugin or creating wrapper command classes. + +### API Changes + +If your migrations are using the `AdapterInterface` to fetch rows or update +rows you will need to update your code. If you use `Adapter::query()` to +execute queries, the return of this method is now +`Cake\Database\StatementInterface` instead. This impacts `fetchAll()`, +and `fetch()`: + +``` php +// This +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll(); + +// Now needs to be +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll('assoc'); +``` + +Similar changes are for fetching a single row: + +``` php +// This +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetch(); + +// Now needs to be +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetch('assoc'); +``` + +## Unified Migrations Table + +As of migrations 5.x, there is a new unified `cake_migrations` table that +replaces the legacy `phinxlog` tables. This provides several benefits: + +- **Single table for all migrations**: Instead of separate `phinxlog` (app) + and `{plugin}_phinxlog` (plugins) tables, all migrations are tracked in + one `cake_migrations` table with a `plugin` column. +- **Simpler database schema**: Fewer migration tracking tables to manage. +- **Better plugin support**: Plugin migrations are properly namespaced. + +### Backward Compatibility + +For existing applications with `phinxlog` tables: + +- **Automatic detection**: If any `phinxlog` table exists, migrations will + continue using the legacy tables automatically. +- **No forced migration**: Existing applications don't need to change anything. +- **Opt-in upgrade**: You can migrate to the new table when you're ready. + +### Configuration + +The `Migrations.legacyTables` configuration option controls the behavior: + +``` php +// config/app.php or config/app_local.php +'Migrations' => [ + // null (default): Autodetect - use legacy if phinxlog tables exist + // false: Force use of new cake_migrations table + // true: Force use of legacy phinxlog tables + 'legacyTables' => null, +], +``` + +### Upgrading to the Unified Table + +To migrate from `phinxlog` tables to the new `cake_migrations` table: + +1. **Preview the upgrade** (dry run): + + ``` bash + bin/cake migrations upgrade --dry-run + ``` + +2. **Run the upgrade**: + + ``` bash + bin/cake migrations upgrade + ``` + +3. **Update your configuration**: + + ``` php + // config/app.php + 'Migrations' => [ + 'legacyTables' => false, + ], + ``` + +4. **Optionally drop phinx tables**: Your migration history is preserved + by default. Use `--drop-tables` to drop the `phinxlog`tables after + verifying your migrations run correctly. + + ``` bash + bin/cake migrations upgrade --drop-tables + ``` + +### Rolling Back + +If you need to revert to phinx tables after upgrading: + +1. Set `'legacyTables' => true` in your configuration. + +> [!WARNING] +> You cannot rollback after running `upgrade --drop-tables`. + +### New Installations + +For new applications without any existing `phinxlog` tables, the unified +`cake_migrations` table is used automatically. No configuration is needed. + +## Problems with the builtin backend? + +If your migrations contain errors when run with the builtin backend, please +open [an issue](https://github.com/cakephp/migrations/issues/new). diff --git a/docs/en/upgrading-to-builtin-backend.rst b/docs/en/upgrading-to-builtin-backend.rst deleted file mode 100644 index ea2909433..000000000 --- a/docs/en/upgrading-to-builtin-backend.rst +++ /dev/null @@ -1,198 +0,0 @@ -Upgrading to the builtin backend -################################ - -As of migrations 4.3 there is a new migrations backend that uses CakePHP's -database abstractions and ORM. In 4.4, the ``builtin`` backend became the -default backend. As of migrations 5.0, phinx has been removed as a dependency -and only the builtin backend is supported. This greatly reduces the dependency -footprint of migrations. - -What is the same? -================= - -Your migrations shouldn't have to change much to adapt to the new backend. -The builtin backend provides similar functionality to what was available with -phinx. If your migrations don't work in a way that could be addressed by the -changes outlined below, please open an issue. - -What is different? -================== - -Command Structure Changes -------------------------- - -As of migrations 5.0, the command structure has changed. The old phinx wrapper -commands have been removed and replaced with new command names: - -**Seeds:** - -.. code-block:: bash - - # Old (4.x and earlier) - bin/cake migrations seed - bin/cake migrations seed --seed Articles - - # New (5.x and later) - bin/cake seeds run - bin/cake seeds run Articles - -The new commands are: - -- ``bin/cake seeds run`` - Run seed classes -- ``bin/cake seeds status`` - Show seed execution status -- ``bin/cake seeds reset`` - Reset seed execution tracking -- ``bin/cake bake seed`` - Generate new seed classes - -Maintaining Backward Compatibility -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -If you need to maintain the old command structure for existing scripts or CI/CD -pipelines, you can add command aliases in your application. In your -``src/Application.php`` file, add the following to the ``console()`` method: - -.. code-block:: php - - public function console(CommandCollection $commands): CommandCollection - { - // Add your application's commands - $commands = $this->addConsoleCommands($commands); - - // Add backward compatibility aliases for migrations 4.x commands - $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); - - return $commands; - } - -For multiple aliases, you can add them all together: - -.. code-block:: php - - // Add multiple backward compatibility aliases - $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); - $commands->add('migrations seed:run', \Migrations\Command\SeedCommand::class); - $commands->add('migrations seed:status', \Migrations\Command\SeedStatusCommand::class); - -This allows gradual migration of scripts and documentation without modifying the -migrations plugin or creating wrapper command classes. - -API Changes ------------ - -If your migrations are using the ``AdapterInterface`` to fetch rows or update -rows you will need to update your code. If you use ``Adapter::query()`` to -execute queries, the return of this method is now -``Cake\Database\StatementInterface`` instead. This impacts ``fetchAll()``, -and ``fetch()``:: - - // This - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetchAll(); - - // Now needs to be - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetchAll('assoc'); - -Similar changes are for fetching a single row:: - - // This - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetch(); - - // Now needs to be - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetch('assoc'); - -Unified Migrations Table -======================== - -As of migrations 5.x, there is a new unified ``cake_migrations`` table that -replaces the legacy ``phinxlog`` tables. This provides several benefits: - -- **Single table for all migrations**: Instead of separate ``phinxlog`` (app) - and ``{plugin}_phinxlog`` (plugins) tables, all migrations are tracked in - one ``cake_migrations`` table with a ``plugin`` column. -- **Simpler database schema**: Fewer migration tracking tables to manage. -- **Better plugin support**: Plugin migrations are properly namespaced. - -Backward Compatibility ----------------------- - -For existing applications with ``phinxlog`` tables: - -- **Automatic detection**: If any ``phinxlog`` table exists, migrations will - continue using the legacy tables automatically. -- **No forced migration**: Existing applications don't need to change anything. -- **Opt-in upgrade**: You can migrate to the new table when you're ready. - -Configuration -------------- - -The ``Migrations.legacyTables`` configuration option controls the behavior: - -.. code-block:: php - - // config/app.php or config/app_local.php - 'Migrations' => [ - // null (default): Autodetect - use legacy if phinxlog tables exist - // false: Force use of new cake_migrations table - // true: Force use of legacy phinxlog tables - 'legacyTables' => null, - ], - -Upgrading to the Unified Table ------------------------------- - -To migrate from ``phinxlog`` tables to the new ``cake_migrations`` table: - -1. **Preview the upgrade** (dry run): - - .. code-block:: bash - - bin/cake migrations upgrade --dry-run - -2. **Run the upgrade**: - - .. code-block:: bash - - bin/cake migrations upgrade - -3. **Update your configuration**: - - .. code-block:: php - - // config/app.php - 'Migrations' => [ - 'legacyTables' => false, - ], - -4. **Optionally drop phinx tables**: Your migration history is preserved - by default. Use ``--drop-tables`` to drop the ``phinxlog``tables after - verifying your migrations run correctly. - - .. code-block:: bash - - bin/cake migrations upgrade --drop-tables - -Rolling Back ------------- - -If you need to revert to phinx tables after upgrading: - -1. Set ``'legacyTables' => true`` in your configuration. - -.. warning:: - - You cannot rollback after running ``upgrade --drop-tables``. - - -New Installations ------------------ - -For new applications without any existing ``phinxlog`` tables, the unified -``cake_migrations`` table is used automatically. No configuration is needed. - -Problems with the builtin backend? -================================== - -If your migrations contain errors when run with the builtin backend, please -open `an issue `_. diff --git a/docs/en/upgrading.md b/docs/en/upgrading.md new file mode 100644 index 000000000..75b95a539 --- /dev/null +++ b/docs/en/upgrading.md @@ -0,0 +1,162 @@ +# Upgrading from 4.x to 5.x + +Migrations 5.x includes significant changes from 4.x. This guide outlines +the breaking changes and what you need to update when upgrading. + +## Requirements + +- **PHP 8.2+** is now required (was PHP 8.1+) +- **CakePHP 5.3+** is now required +- **Phinx has been removed** - The builtin backend is now the only supported backend + +If you were already using the builtin backend in 4.x (introduced in 4.3, default in 4.4), +the upgrade should be straightforward. See [Upgrading to the builtin backend](upgrading-to-builtin-backend) for more +details on API differences between the phinx and builtin backends. + +## Command Changes + +The phinx wrapper commands have been removed. The new command structure is: + +### Migrations + +The migration commands remain unchanged: + +``` bash +bin/cake migrations migrate +bin/cake migrations rollback +bin/cake migrations status +bin/cake migrations mark_migrated +bin/cake migrations dump +``` + +### Seeds + +Seed commands have changed: + +``` bash +# 4.x # 5.x +bin/cake migrations seed bin/cake seeds run +bin/cake migrations seed --seed X bin/cake seeds run X +``` + +The new seed commands are: + +- `bin/cake seeds run` - Run seed classes +- `bin/cake seeds run SeedName` - Run a specific seed +- `bin/cake seeds status` - Show seed execution status +- `bin/cake seeds reset` - Reset seed execution tracking + +### Maintaining Backward Compatibility + +If you need to maintain the old `migrations seed` command for existing scripts or +CI/CD pipelines, you can add command aliases in your `src/Application.php`: + +``` php +public function console(CommandCollection $commands): CommandCollection +{ + $commands = $this->addConsoleCommands($commands); + + // Add backward compatibility alias + $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); + + return $commands; +} +``` + +## Removed Classes and Namespaces + +The following have been removed in 5.x: + +- `Migrations\Command\Phinx\*` - All phinx wrapper commands +- `Migrations\Command\MigrationsCommand` - Use `bin/cake migrations` entry point +- `Migrations\Command\MigrationsSeedCommand` - Use `bin/cake seeds run` +- `Migrations\Command\MigrationsCacheBuildCommand` - Schema cache is managed differently +- `Migrations\Command\MigrationsCacheClearCommand` - Schema cache is managed differently +- `Migrations\Command\MigrationsCreateCommand` - Use `bin/cake bake migration` + +If you have code that directly references any of these classes, you will need to update it. + +## API Changes + +### Adapter Query Results + +If your migrations use `AdapterInterface::query()` to fetch rows, the return type has +changed from a phinx result to `Cake\Database\StatementInterface`: + +``` php +// 4.x (phinx) +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll(); +$row = $stmt->fetch(); + +// 5.x (builtin) +$stmt = $this->getAdapter()->query('SELECT * FROM articles'); +$rows = $stmt->fetchAll('assoc'); +$row = $stmt->fetch('assoc'); +``` + +## New Features in 5.x + +5.x includes several new features: + +### Seed Tracking + +Seeds are now tracked in a `cake_seeds` table by default, preventing accidental re-runs. +Use `--force` to run a seed again, or `bin/cake seeds reset` to clear tracking. +See [Database Seeding](seeding) for more details. + +### Check Constraints + +Support for database check constraints via `addCheckConstraint()`. +See [Writing Migrations](writing-migrations) for usage details. + +### MySQL ALTER Options + +Support for `ALGORITHM` and `LOCK` options on MySQL ALTER TABLE operations, +allowing control over how MySQL performs schema changes. + +### 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 + +Your existing migration files should work without changes in most cases. The builtin backend +provides the same API as phinx for common operations. + +If you encounter issues with existing migrations, please report them at + diff --git a/docs/en/using-the-query-builder.md b/docs/en/using-the-query-builder.md new file mode 100644 index 000000000..297d11359 --- /dev/null +++ b/docs/en/using-the-query-builder.md @@ -0,0 +1,260 @@ +# Using the Query Builder + +It is not uncommon to pair database structure changes with data changes. For example, you may want to +migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, +Migrations provides access to a Query builder object, that you may use to execute complex `SELECT`, `UPDATE`, +`INSERT` or `DELETE` statements. + +The Query builder is provided by the [cakephp/database](https://github.com/cakephp/database) project, and should +be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the +`getQueryBuilder(string $type)` function. The `string $type` options are 'select', 'insert', 'update' and \`'delete'\`: + +``` php +getQueryBuilder('select'); + $statement = $builder->select('*')->from('users')->execute(); + var_dump($statement->fetchAll()); + } +} +``` + +## Selecting Fields + +Adding fields to the SELECT clause: + +``` php +select(['id', 'title', 'body']); + +// Results in SELECT id AS pk, title AS aliased_title, body ... +$builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); + +// Use a closure +$builder->select(function ($builder) { + return ['id', 'title', 'body']; +}); +``` + +## Where Conditions + +Generating conditions: + +``` php +// WHERE id = 1 +$builder->where(['id' => 1]); + +// WHERE id > 1 +$builder->where(['id >' => 1]); +``` + +As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well: + +``` php +where(['id >' => 1])->andWhere(['title' => 'My Title']); + +// Equivalent to +$builder->where(['id >' => 1, 'title' => 'My title']); + +// WHERE id > 1 OR title = 'My title' +$builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); +``` + +For even more complex conditions you can use closures and expression objects: + +``` php +select('*') + ->from('articles') + ->where(function ($exp) { + return $exp + ->eq('author_id', 2) + ->eq('published', true) + ->notEq('spam', true) + ->gt('view_count', 10); + }); +``` + +Which results in: + +``` sql +SELECT * FROM articles +WHERE + author_id = 2 + AND published = 1 + AND spam != 1 + AND view_count > 10 +``` + +Combining expressions is also possible: + +``` php +select('*') + ->from('articles') + ->where(function ($exp) { + $orConditions = $exp->or_(['author_id' => 2]) + ->eq('author_id', 5); + return $exp + ->not($orConditions) + ->lte('view_count', 10); + }); +``` + +It generates: + +``` sql +SELECT * +FROM articles +WHERE + NOT (author_id = 2 OR author_id = 5) + AND view_count <= 10 +``` + +When using the expression objects you can use the following methods to create conditions: + +- `eq()` Creates an equality condition. +- `notEq()` Create an inequality condition +- `like()` Create a condition using the `LIKE` operator. +- `notLike()` Create a negated `LIKE` condition. +- `in()` Create a condition using `IN`. +- `notIn()` Create a negated condition using `IN`. +- `gt()` Create a `>` condition. +- `gte()` Create a `>=` condition. +- `lt()` Create a `<` condition. +- `lte()` Create a `<=` condition. +- `isNull()` Create an `IS NULL` condition. +- `isNotNull()` Create a negated `IS NULL` condition. + +## Aggregates and SQL Functions + +``` php +select(['count' => $builder->func()->count('*')]); +``` + +A number of commonly used functions can be created with the func() method: + +- `sum()` Calculate a sum. The arguments will be treated as literal values. +- `avg()` Calculate an average. The arguments will be treated as literal values. +- `min()` Calculate the min of a column. The arguments will be treated as literal values. +- `max()` Calculate the max of a column. The arguments will be treated as literal values. +- `count()` Calculate the count. The arguments will be treated as literal values. +- `concat()` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. +- `coalesce()` Coalesce values. The arguments are treated as bound parameters unless marked as literal. +- `dateDiff()` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. +- `now()` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. + +When providing arguments for SQL functions, there are two kinds of parameters you can use, +literal arguments and bound parameters. Literal parameters allow you to reference columns or +other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: + +``` php +func()->concat([ + 'title' => 'literal', + ' NEW' +]); +$query->select(['title' => $concat]); +``` + +## Getting Results out of a Query + +Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: + +``` php +execute()->fetchAll('assoc'); +``` + +## Creating an Insert Query + +Creating insert queries is also possible: + +``` php +getQueryBuilder('insert'); +$builder + ->insert(['first_name', 'last_name']) + ->into('users') + ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) + ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) + ->execute(); +``` + +For increased performance, you can use another builder object as the values for an insert query: + +``` php +getQueryBuilder('select'); +$namesQuery + ->select(['fname', 'lname']) + ->from('users') + ->where(['is_active' => true]); + +$builder = $this->getQueryBuilder('insert'); +$st = $builder + ->insert(['first_name', 'last_name']) + ->into('names') + ->values($namesQuery) + ->execute(); + +var_dump($st->lastInsertId('names', 'id')); +``` + +The above code will generate: + +``` sql +INSERT INTO names (first_name, last_name) + (SELECT fname, lname FROM USERS where is_active = 1) +``` + +## Creating an update Query + +Creating update queries is similar to both inserting and selecting: + +``` php +getQueryBuilder('update'); +$builder + ->update('users') + ->set('fname', 'Snow') + ->where(['fname' => 'Jon']) + ->execute(); +``` + +## Creating a Delete Query + +Finally, delete queries: + +``` php +getQueryBuilder('delete'); +$builder + ->delete('users') + ->where(['accepted_gdpr' => false]) + ->execute(); +``` diff --git a/docs/en/using-the-query-builder.rst b/docs/en/using-the-query-builder.rst deleted file mode 100644 index 64a7617cb..000000000 --- a/docs/en/using-the-query-builder.rst +++ /dev/null @@ -1,272 +0,0 @@ -Using the Query Builder -####################### - -It is not uncommon to pair database structure changes with data changes. For example, you may want to -migrate the data in a couple columns from the users to a newly created table. For this type of scenarios, -Migrations provides access to a Query builder object, that you may use to execute complex ``SELECT``, ``UPDATE``, -``INSERT`` or ``DELETE`` statements. - -The Query builder is provided by the `cakephp/database `_ project, and should -be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the -``getQueryBuilder(string $type)`` function. The ``string $type`` options are `'select'`, `'insert'`, `'update'` and `'delete'`:: - - getQueryBuilder('select'); - $statement = $builder->select('*')->from('users')->execute(); - var_dump($statement->fetchAll()); - } - } - -Selecting Fields ----------------- - -Adding fields to the SELECT clause:: - - select(['id', 'title', 'body']); - - // Results in SELECT id AS pk, title AS aliased_title, body ... - $builder->select(['pk' => 'id', 'aliased_title' => 'title', 'body']); - - // Use a closure - $builder->select(function ($builder) { - return ['id', 'title', 'body']; - }); - - -Where Conditions ----------------- - -Generating conditions:: - - // WHERE id = 1 - $builder->where(['id' => 1]); - - // WHERE id > 1 - $builder->where(['id >' => 1]); - - -As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well:: - - where(['id >' => 1])->andWhere(['title' => 'My Title']); - - // Equivalent to - $builder->where(['id >' => 1, 'title' => 'My title']); - - // WHERE id > 1 OR title = 'My title' - $builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); - - -For even more complex conditions you can use closures and expression objects:: - - select('*') - ->from('articles') - ->where(function ($exp) { - return $exp - ->eq('author_id', 2) - ->eq('published', true) - ->notEq('spam', true) - ->gt('view_count', 10); - }); - - -Which results in: - -.. code-block:: sql - - SELECT * FROM articles - WHERE - author_id = 2 - AND published = 1 - AND spam != 1 - AND view_count > 10 - - -Combining expressions is also possible:: - - select('*') - ->from('articles') - ->where(function ($exp) { - $orConditions = $exp->or_(['author_id' => 2]) - ->eq('author_id', 5); - return $exp - ->not($orConditions) - ->lte('view_count', 10); - }); - -It generates: - -.. code-block:: sql - - SELECT * - FROM articles - WHERE - NOT (author_id = 2 OR author_id = 5) - AND view_count <= 10 - - -When using the expression objects you can use the following methods to create conditions: - -* ``eq()`` Creates an equality condition. -* ``notEq()`` Create an inequality condition -* ``like()`` Create a condition using the ``LIKE`` operator. -* ``notLike()`` Create a negated ``LIKE`` condition. -* ``in()`` Create a condition using ``IN``. -* ``notIn()`` Create a negated condition using ``IN``. -* ``gt()`` Create a ``>`` condition. -* ``gte()`` Create a ``>=`` condition. -* ``lt()`` Create a ``<`` condition. -* ``lte()`` Create a ``<=`` condition. -* ``isNull()`` Create an ``IS NULL`` condition. -* ``isNotNull()`` Create a negated ``IS NULL`` condition. - - -Aggregates and SQL Functions ----------------------------- - -.. code-block:: php - - select(['count' => $builder->func()->count('*')]); - -A number of commonly used functions can be created with the func() method: - -* ``sum()`` Calculate a sum. The arguments will be treated as literal values. -* ``avg()`` Calculate an average. The arguments will be treated as literal values. -* ``min()`` Calculate the min of a column. The arguments will be treated as literal values. -* ``max()`` Calculate the max of a column. The arguments will be treated as literal values. -* ``count()`` Calculate the count. The arguments will be treated as literal values. -* ``concat()`` Concatenate two values together. The arguments are treated as bound parameters unless marked as literal. -* ``coalesce()`` Coalesce values. The arguments are treated as bound parameters unless marked as literal. -* ``dateDiff()`` Get the difference between two dates/times. The arguments are treated as bound parameters unless marked as literal. -* ``now()`` Take either 'time' or 'date' as an argument allowing you to get either the current time, or current date. - -When providing arguments for SQL functions, there are two kinds of parameters you can use, -literal arguments and bound parameters. Literal parameters allow you to reference columns or -other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: - - -.. code-block:: php - - func()->concat([ - 'title' => 'literal', - ' NEW' - ]); - $query->select(['title' => $concat]); - - -Getting Results out of a Query ------------------------------- - -Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: - - -.. code-block:: php - - execute()->fetchAll('assoc'); - - -Creating an Insert Query ------------------------- - -Creating insert queries is also possible: - - -.. code-block:: php - - getQueryBuilder('insert'); - $builder - ->insert(['first_name', 'last_name']) - ->into('users') - ->values(['first_name' => 'Steve', 'last_name' => 'Jobs']) - ->values(['first_name' => 'Jon', 'last_name' => 'Snow']) - ->execute(); - - -For increased performance, you can use another builder object as the values for an insert query: - -.. code-block:: php - - getQueryBuilder('select'); - $namesQuery - ->select(['fname', 'lname']) - ->from('users') - ->where(['is_active' => true]); - - $builder = $this->getQueryBuilder('insert'); - $st = $builder - ->insert(['first_name', 'last_name']) - ->into('names') - ->values($namesQuery) - ->execute(); - - var_dump($st->lastInsertId('names', 'id')); - - -The above code will generate: - -.. code-block:: sql - - INSERT INTO names (first_name, last_name) - (SELECT fname, lname FROM USERS where is_active = 1) - - -Creating an update Query ------------------------- - -Creating update queries is similar to both inserting and selecting: - -.. code-block:: php - - getQueryBuilder('update'); - $builder - ->update('users') - ->set('fname', 'Snow') - ->where(['fname' => 'Jon']) - ->execute(); - - -Creating a Delete Query ------------------------ - -Finally, delete queries: - -.. code-block:: php - - getQueryBuilder('delete'); - $builder - ->delete('users') - ->where(['accepted_gdpr' => false]) - ->execute(); diff --git a/docs/en/writing-migrations.md b/docs/en/writing-migrations.md new file mode 100644 index 000000000..162515f1b --- /dev/null +++ b/docs/en/writing-migrations.md @@ -0,0 +1,2527 @@ +# Writing Migrations + +Migrations are a declarative API that helps you transform your database. Each migration +is represented by a PHP class in a unique file. It is preferred that you write +your migrations using the Migrations API, but raw SQL is also supported. + +## Creating a New Migration + +Let's start by creating a new migration with `bake`: + +``` bash +$ bin/cake bake migration +``` + +This will create a new migration in the format +`YYYYMMDDHHMMSS_my_new_migration.php`, where the first 14 characters are +replaced with the current timestamp down to the second. + +If you have specified multiple migration paths, you will be asked to select +which path to create the new migration in. + +Bake will automatically creates a skeleton migration file with a single method: + +``` php + [ + 'style' => 'anonymous', // or 'traditional' +], +``` + +This configuration also applies to seeds, allowing you to use consistent styling +across your entire project. + +## The Change Method + +Migrations supports 'reversible migrations'. In many scenarios, you +only need to define the `up` logic, and Migrations can figure out how to +generate the rollback operations for you. For example: + +``` php +table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } +} +``` + +When executing this migration, Migrations will create the `user_logins` table on +the way up and automatically figure out how to drop the table on the way down. +Please be aware that when a `change` method exists, Migrations will +ignore the `up` and `down` methods. If you need to use these methods it is +recommended to create a separate migration file. + +> [!NOTE] +> When creating or updating tables inside a `change()` method you must use +> the Table `create()` and `update()` methods. Migrations cannot automatically +> determine whether a `save()` call is creating a new table or modifying an +> existing one. + +The following actions are reversible when done through the Table API in +Migrations, and will be automatically reversed: + +- Creating a table +- Renaming a table +- Adding a column +- Renaming a column +- Adding an index +- Adding a foreign key +- Adding a check constraint + +If a command cannot be reversed then Migrations will throw an +`IrreversibleMigrationException` when it's migrating down. If you wish to +use a command that cannot be reversed in the change function, you can use an +if statement with `$this->isMigratingUp()` to only run things in the +up or down direction. For example: + +``` php +table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + if ($this->isMigratingUp()) { + $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) + ->save(); + } + } +} +``` + +## The Up Method + +The up method is automatically run by Migrations when you are migrating up and it +detects the given migration hasn't been executed previously. You should use the +up method to transform the database with your intended changes. + +## The Down Method + +The down method is automatically run by Migrations when you are migrating down and +it detects the given migration has been executed in the past. You should use +the down method to reverse/undo the transformations described in the up method. + +## The Init Method + +The `init()` method is run by Migrations before the migration methods if it exists. +This can be used for setting common class properties that are then used within +the migration methods. + +## The Should Execute Method + +The `shouldExecute()` method is run by Migrations before executing the migration. +This can be used to prevent the migration from being executed at this time. It always +returns true by default. You can override it in your custom `BaseMigration` +implementation. + +## Working With Tables + +The Table object enables you to easily manipulate database tables using PHP +code. You can retrieve an instance of the Table object by calling the +`table()` method from within your database migration: + +``` php +table('tableName'); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +You can then manipulate this table using the methods provided by the Table +object. + + + +## Adding Columns + +Column types are specified as strings and can be one of: + +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- double +- smallinteger +- integer +- biginteger +- string +- text +- time +- timestamp +- uuid +- binaryuuid +- nativeuuid + +In addition, the MySQL adapter supports `enum`, `set`, `blob`, +`tinyblob`, `mediumblob`, `longblob`, `bit` and `json` column types +(`json` in MySQL 5.7 and above). When providing a limit value and using +`binary`, `varbinary` or `blob` and its subtypes, the retained column type +will be based on required length (see [Limit Option and MySQL](#limit-option-and-mysql) for details). + +With most adapters, the `uuid` and `nativeuuid` column types are aliases, +however with the MySQL adapter + MariaDB, the `nativeuuid` type maps to +a native uuid column instead of `CHAR(36)` like `uuid` does. + +In addition, the Postgres adapter supports `interval`, `json`, `jsonb`, +`uuid`, `cidr`, `inet` and `macaddr` column types (PostgreSQL 9.3 and +above). + +### Valid Column Options + +The following are valid column options: + +For any column type: + +| Option | Description | +|----|----| +| limit | set maximum length for strings, also hints column types in adapters (see note below) | +| length | alias for `limit` | +| default | set default value or action | +| null | allow `NULL` values, defaults to `true` (setting `identity` will override default to `false`) | +| after | specify the column that a new column should be placed after, or use `\Migrations\Db\Adapter\MysqlAdapter::FIRST` to place the column at the start of the table *(only applies to MySQL)* | +| comment | set a text comment on the column | + +For `decimal` and `float` columns: + +| Option | Description | +|----|----| +| precision | total number of digits (e.g., 10 in `DECIMAL(10,2)`) | +| scale | number of digits after the decimal point (e.g., 2 in `DECIMAL(10,2)`) | +| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | + +> [!NOTE] +> **Precision and Scale Terminology** +> +> Migrations follows the SQL standard where `precision` represents the total number of digits, +> and `scale` represents digits after the decimal point. For example, to create `DECIMAL(10,2)` +> (10 total digits with 2 decimal places): +> +> ``` php +> $table->addColumn('price', 'decimal', [ +> 'precision' => 10, // Total digits +> 'scale' => 2, // Decimal places +> ]); +> ``` +> +> This differs from CakePHP's TableSchema which uses `length` for total digits and +> `precision` for decimal places. The migration adapter handles this conversion automatically. + +For `enum` and `set` columns: + +| Option | Description | +|--------|-----------------------------------------------------| +| values | Can be a comma separated list or an array of values | + +For `smallinteger`, `integer` and `biginteger` columns: + +| Option | Description | +|----|----| +| identity | enable or disable automatic incrementing (if enabled, will set `null: false` if `null` option is not set) | +| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | + +For Postgres, when using `identity`, it will utilize the `serial` type +appropriate for the integer size, so that `smallinteger` will give you +`smallserial`, `integer` gives `serial`, and `biginteger` gives +`bigserial`. + +For `date` columns: + +| Option | Description | +|---------|---------------------------------------------| +| default | set default value (use with `CURRENT_DATE`) | + +For `time` columns: + +| Option | Description | +|----|----| +| default | set default value (use with `CURRENT_TIME`) | +| timezone | enable or disable the `with time zone` option *(only applies to Postgres)* | + +For `datetime` columns: + +| Option | Description | +|----|----| +| default | set default value (use with `CURRENT_TIMESTAMP`) | +| timezone | enable or disable the `with time zone` option *(only applies to Postgres)* | + +For `timestamp` columns: + +| Option | Description | +|----|----| +| default | set default value (use with `CURRENT_TIMESTAMP`) | +| update | set an action to be triggered when the row is updated (use with `CURRENT_TIMESTAMP`) *(only applies to MySQL)* | +| timezone | enable or disable the `with time zone` option for `time` and `timestamp` columns *(only applies to Postgres)* | + +You can add `created` and `updated` timestamps to a table using the +`addTimestamps()` method. This method accepts three arguments, where the first +two allow setting alternative names for the columns while the third argument +allows you to enable the `timezone` option for the columns. The defaults for +these arguments are `created`, `updated`, and `false` respectively. For +the first and second argument, if you provide `null`, then the default name +will be used, and if you provide `false`, then that column will not be +created. Please note that attempting to set both to `false` will throw +a `\RuntimeException`. Additionally, you can use the +`addTimestampsWithTimezone()` method, which is an alias to `addTimestamps()` +that will always set the third argument to `true` (see examples below). The +`created` column will have a default set to `CURRENT_TIMESTAMP`. For MySQL +only, `updated` column will have update set to +`CURRENT_TIMESTAMP`: + +``` php +table('users')->addTimestamps()->create(); + // Use defaults (with timezones) + $table = $this->table('users')->addTimestampsWithTimezone()->create(); + + // Override the 'created' column name with 'recorded_at'. + $table = $this->table('books')->addTimestamps('recorded_at')->create(); + + // Override the 'updated' column name with 'amended_at', preserving timezones. + // The two lines below do the same, the second one is simply cleaner. + $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); + $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); + + // Only add the created column to the table + $table = $this->table('books')->addTimestamps(null, false); + // Only add the updated column to the table + $table = $this->table('users')->addTimestamps(false); + // Note, setting both false will throw a \RuntimeError + } +} +``` + +For `boolean` columns: + +| Option | Description | +|--------|-------------------------------------------------------------------| +| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | + +For `string` and `text` columns: + +| Option | Description | +|----|----| +| collation | set collation that differs from table defaults *(only applies to MySQL)* | +| encoding | set character set that differs from table defaults *(only applies to MySQL)* | + +### Limit Option and MySQL + +When using the MySQL adapter, there are a couple things to consider when working with limits: + +- When using a `string` primary key or index on MySQL 5.7 or below, or the + MyISAM storage engine, and the default charset of `utf8mb4_unicode_ci`, you + must specify a limit less than or equal to 191, or use a different charset. +- Additional hinting of database column type can be made for `integer`, + `text`, `blob`, `tinyblob`, `mediumblob`, `longblob` columns. Using + `limit` with one the following options will modify the column type + accordingly: + +| Limit | Column Type | +|--------------|-------------| +| BLOB_TINY | TINYBLOB | +| BLOB_REGULAR | BLOB | +| BLOB_MEDIUM | MEDIUMBLOB | +| BLOB_LONG | LONGBLOB | +| TEXT_TINY | TINYTEXT | +| TEXT_REGULAR | TEXT | +| TEXT_MEDIUM | MEDIUMTEXT | +| TEXT_LONG | LONGTEXT | +| INT_TINY | TINYINT | +| INT_SMALL | SMALLINT | +| INT_MEDIUM | MEDIUMINT | +| INT_REGULAR | INT | +| INT_BIG | BIGINT | + +For `binary` or `varbinary` types, if limit is set greater than allowed 255 +bytes, the type will be changed to the best matching blob type given the +length: + +``` php +table('cart_items'); +$table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); +``` + +### Default values with expressions + +If you need to set a default to an expression, you can use a `Literal` to have +the column's default value used without any quoting or escaping. This is helpful +when you want to use a function as a default value: + +``` php +use Migrations\BaseMigration; +use Migrations\Db\Literal; + +class AddSomeColumns extends BaseMigration +{ + public function change(): void + { + $this->table('users') + ->addColumn('uniqid', 'uuid', [ + 'default' => Literal::from('uuid_generate_v4()') + ]) + ->create(); + } +} +``` + + + +### Creating a Table + +Creating a table is really easy using the Table object. Let's create a table to +store a collection of users: + +``` php +table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->create(); + } +} +``` + +Columns are added using the `addColumn()` method. We create a unique index +for both the username and email columns using the `addIndex()` method. +Finally calling `create()` commits the changes to the database. + +> [!NOTE] +> Migrations automatically creates an auto-incrementing primary key column called `id` for every +> table. + +The `id` option sets the name of the automatically created identity field, +while the `primary_key` option selects the field or fields used for primary +key. `id` will always override the `primary_key` option unless it's set to +false. If you don't need a primary key set `id` to false without specifying +a `primary_key`, and no primary key will be created. + +To specify an alternate primary key, you can specify the `primary_key` option +when accessing the Table object. Let's disable the automatic `id` column and +create a primary key using two columns instead: + +``` php +table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } +} +``` + +Setting a single `primary_key` doesn't enable the `AUTO_INCREMENT` option. +To simply change the name of the primary key, we need to override the default `id` field name: + +``` php +table('followers', ['id' => 'user_id']); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } +} +``` + +In addition, the MySQL adapter supports following options: + +| Option | Platform | Description | +|----|----|----| +| comment | MySQL, Postgres | set a text comment on the table | +| collation | MySQL, SqlServer | set the table collation *(defaults to database collation)* | +| row_format | MySQL | set the table row format | +| engine | MySQL | define table engine *(defaults to \`\`InnoDB\`\`)* | +| signed | MySQL | whether the primary key is `signed` *(defaults to \`\`true\`\`)* | +| limit | MySQL | set the maximum length for the primary key | + +By default, the primary key is `signed`. +To set it to be unsigned, pass the `signed` option with a `false` +value, or enable the `Migrations.unsigned_primary_keys` and +`Migrations.unsigned_ints` feature flags (see [Feature Flags](index#feature-flags)). +Both flags should be used together so that foreign key columns match +the primary keys they reference: + +``` php +table('followers', ['signed' => false]); + $table->addColumn('follower_id', 'integer') + ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) + ->create(); + } +} +``` + +If you need to create a table with a different collation than the database, +use: + +``` php +table('categories', [ + 'collation' => 'latin1_german1_ci' + ]) + ->addColumn('title', 'string') + ->create(); + } +} +``` + +Note however this can only be done on table creation : there is currently no way +of adding a column to an existing table with a different collation than the +table or the database. Only `MySQL` and `SqlServer` supports this +configuration key for the time being. + +To view available column types and options, see [Adding Columns](#adding-columns) for details. + +### MySQL ALTER TABLE Options + +::: info Added in version 5.0.0 +`ALGORITHM` and `LOCK` options were added in 5.0.0. +::: + +When modifying tables in MySQL, you can control how the ALTER TABLE operation is +performed using the `algorithm` and `lock` options. This is useful for performing +zero-downtime schema changes on large tables in production environments. + +``` php +table('large_table'); + $table->addIndex(['status'], [ + 'name' => 'idx_status', + ]); + $table->update([ + 'algorithm' => 'INPLACE', + 'lock' => 'NONE', + ]); + } +} +``` + +Available `algorithm` values: + +| Algorithm | Description | +|----|----| +| DEFAULT | Let MySQL choose the algorithm (default behavior) | +| INPLACE | Modify the table in place without copying data (when possible) | +| COPY | Create a copy of the table with the changes (legacy method) | +| INSTANT | Only modify metadata, no table rebuild (MySQL 8.0+, limited operations) | + +Available `lock` values: + +| Lock | Description | +|-----------|----------------------------------------------------------| +| DEFAULT | Use minimal locking for the algorithm (default behavior) | +| NONE | Allow concurrent reads and writes during the operation | +| SHARED | Allow concurrent reads but block writes | +| EXCLUSIVE | Block all reads and writes during the operation | + +> [!NOTE] +> Not all operations support all algorithm/lock combinations. MySQL will raise +> an error if the requested combination is not possible for the operation. +> The `INSTANT` algorithm is only available in MySQL 8.0+ and only for specific +> operations like adding columns at the end of a table. + +> [!WARNING] +> Using `ALGORITHM=INPLACE, LOCK=NONE` does not guarantee zero-downtime for +> all operations. Some operations may still require a table copy or exclusive lock. +> Always test schema changes on a staging environment first. + +### Table Partitioning + +Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning helps +manage large tables by splitting them into smaller, more manageable pieces. + +> [!NOTE] +> Partition columns must be included in the primary key for MySQL. SQLite does +> not support partitioning. MySQL's `RANGE` and `LIST` types only work with +> integer columns - use `RANGE COLUMNS` and `LIST COLUMNS` for DATE/STRING columns. + +#### RANGE Partitioning + +RANGE partitioning is useful when you want to partition by numeric ranges. For MySQL, +use `TYPE_RANGE` with integer columns or expressions, and `TYPE_RANGE_COLUMNS` for +DATE/DATETIME/STRING columns: + +``` php +table('orders', [ + 'id' => false, + 'primary_key' => ['id', 'order_date'], + ]); + $table->addColumn('id', 'integer', ['identity' => true]) + ->addColumn('order_date', 'date') + ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2]) + ->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date') + ->addPartition('p2022', '2023-01-01') + ->addPartition('p2023', '2024-01-01') + ->addPartition('p2024', '2025-01-01') + ->addPartition('pmax', 'MAXVALUE') + ->create(); + } +} +``` + +#### LIST Partitioning + +LIST partitioning is useful when you want to partition by discrete values. For MySQL, +use `TYPE_LIST` with integer columns and `TYPE_LIST_COLUMNS` for STRING columns: + +``` php +table('customers', [ + 'id' => false, + 'primary_key' => ['id', 'region'], + ]); + $table->addColumn('id', 'integer', ['identity' => true]) + ->addColumn('region', 'string', ['limit' => 20]) + ->addColumn('name', 'string') + ->partitionBy(Partition::TYPE_LIST_COLUMNS, 'region') + ->addPartition('p_americas', ['US', 'CA', 'MX', 'BR']) + ->addPartition('p_europe', ['UK', 'DE', 'FR', 'IT']) + ->addPartition('p_asia', ['JP', 'CN', 'IN', 'KR']) + ->create(); + } +} +``` + +#### HASH Partitioning + +HASH partitioning distributes data evenly across a specified number of partitions: + +``` php +table('sessions'); + $table->addColumn('user_id', 'integer') + ->addColumn('data', 'text') + ->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8]) + ->create(); + } +} +``` + +#### KEY Partitioning (MySQL only) + +KEY partitioning is similar to HASH but uses MySQL's internal hashing function: + +``` php +table('cache', [ + 'id' => false, + 'primary_key' => ['cache_key'], + ]); + $table->addColumn('cache_key', 'string', ['limit' => 255]) + ->addColumn('value', 'binary') + ->partitionBy(Partition::TYPE_KEY, 'cache_key', ['count' => 16]) + ->create(); + } +} +``` + +#### Partitioning with Expressions + +You can partition by expressions using the `Literal` class: + +``` php +table('events', [ + 'id' => false, + 'primary_key' => ['id', 'created_at'], + ]); + $table->addColumn('id', 'integer', ['identity' => true]) + ->addColumn('created_at', 'datetime') + ->partitionBy(Partition::TYPE_RANGE, Literal::from('YEAR(created_at)')) + ->addPartition('p2022', 2023) + ->addPartition('p2023', 2024) + ->addPartition('pmax', 'MAXVALUE') + ->create(); + } +} +``` + +#### Modifying Partitions on Existing Tables + +You can add or drop partitions on existing partitioned tables: + +``` php +table('orders') + ->addPartitionToExisting('p2025', '2026-01-01') + ->update(); + } + + public function down(): void + { + // Drop the partition + $this->table('orders') + ->dropPartition('p2025') + ->update(); + } +} +``` + +### Saving Changes + +When working with the Table object, Migrations stores certain operations in a +pending changes cache. Once you have made the changes you want to the table, +you must save them. To perform this operation, Migrations provides three methods, +`create()`, `update()`, and `save()`. `create()` will first create +the table and then run the pending changes. `update()` will just run the +pending changes, and should be used when the table already exists. `save()` +is a helper function that checks first if the table exists and if it does not +will run `create()`, else it will run `update()`. + +As stated above, when using the `change()` migration method, you should always +use `create()` or `update()`, and never `save()` as otherwise migrating +and rolling back may result in different states, due to `save()` calling +`create()` when running migrate and then `update()` on rollback. When +using the `up()`/`down()` methods, it is safe to use either `save()` or +the more explicit methods. + +When in doubt with working with tables, it is always recommended to call +the appropriate function and commit any pending changes to the database. + +### Renaming a Column + +To rename a column, access an instance of the Table object then call the +`renameColumn()` method: + +``` php +table('users'); + $table->renameColumn('bio', 'biography') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('users'); + $table->renameColumn('biography', 'bio') + ->save(); + } +} +``` + +### Adding a Column After Another Column + +When adding a column with the MySQL adapter, you can dictate its position using +the `after` option, where its value is the name of the column to position it +after: + +``` php +table('users'); + $table->addColumn('city', 'string', ['after' => 'email']) + ->update(); + } +} +``` + +This would create the new column `city` and position it after the `email` +column. The `\Migrations\Db\Adapter\MysqlAdapter::FIRST` constant can be used +to specify that the new column should be created as the first column in that +table. + +### Dropping a Column + +To drop a column, use the `removeColumn()` method: + +``` php +table('users'); + $table->removeColumn('short_name') + ->save(); + } +} +``` + +### Specifying a Column Limit + +You can limit the maximum length of a column by using the `limit` option: + +``` php +table('tags'); + $table->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); + } +} +``` + +### Changing Column Attributes + +There are two methods for modifying existing columns: + +#### Updating Columns (Recommended) + +To modify specific column attributes while preserving others, use the `updateColumn()` method. +This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.: + +``` php +table('users'); + // Make email nullable, preserving all other attributes + $users->updateColumn('email', null, ['null' => true]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $users = $this->table('users'); + $users->updateColumn('email', null, ['null' => false]) + ->save(); + } +} +``` + +You can pass `null` as the column type to preserve the existing type, or specify a new type: + +``` php +// Preserve type and other attributes, only change nullability +$table->updateColumn('email', null, ['null' => true]); + +// Change type to biginteger, preserve default and other attributes +$table->updateColumn('user_id', 'biginteger'); + +// Change default value, preserve everything else +$table->updateColumn('status', null, ['default' => 'active']); +``` + +The following attributes are automatically preserved by `updateColumn()`: + +- Default values +- NULL/NOT NULL constraint +- Column limit/length +- Decimal scale/precision +- Comments +- Signed/unsigned (for numeric types) +- Collation and encoding +- Enum/set values + +#### Changing Columns (Traditional) + +To completely replace a column definition, use the `changeColumn()` method. +This method requires you to specify all desired column attributes. +See [Valid Column Types](#valid-column-types) and [Valid Column Options](#valid-column-options) for allowed values: + +``` php +table('users'); + // Must specify all attributes + $users->changeColumn('email', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null, + ]) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +You can enable attribute preservation with `changeColumn()` by passing +`'preserveUnspecified' => true` in the options: + +``` php +$table->changeColumn('email', 'string', [ + 'null' => true, + 'preserveUnspecified' => true, +]); +``` + +> [!NOTE] +> For most use cases, `updateColumn()` is recommended as it is safer and requires +> less code. Use `changeColumn()` when you need to completely redefine a column +> or when working with legacy code that expects the traditional behavior. + +### Working With Indexes + +To add an index to a table you can simply call the `addIndex()` method on the +table object: + +``` php +table('users'); + $table->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +By default Migrations instructs the database adapter to create a simple index. We +can pass an additional parameter `unique` to the `addIndex()` method to +specify a unique index. We can also explicitly specify a name for the index +using the `name` parameter, the index columns sort order can also be specified using +the `order` parameter. The order parameter takes an array of column names and sort order key/value pairs: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex(['email', 'username'], [ + 'unique' => true, + 'name' => 'idx_users_email', + 'order' => ['email' => 'DESC', 'username' => 'ASC']] + ) + ->save(); + } +} +``` + +As of 4.6.0, you can use `BaseMigration::index()` to get a fluent builder to +define indexes: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addIndex( + $this->index(['email', 'username']) + ->setType('unique') + ->setName('idx_users_email') + ->setOrder(['email' => 'DESC', 'username' => 'ASC']) + ) + ->save(); + } +} +``` + +The MySQL adapter also supports `fulltext` indexes. If you are using a version before 5.6 you must +ensure the table uses the `MyISAM` engine: + +``` php +table('users', ['engine' => 'MyISAM']); + $table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); + } +} +``` + +MySQL adapter supports setting the index length defined by limit option. +When you are using a multi-column index, you are able to define each column index length. +The single column index can define its index length with or without defining column name in limit option: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('username','string') + ->addColumn('user_guid', 'string', ['limit' => 36]) + ->addIndex(['email','username'], ['limit' => ['email' => 5, 'username' => 2]]) + ->addIndex('user_guid', ['limit' => 6]) + ->create(); + } +} +``` + +The SQL Server and PostgreSQL adapters support `include` (non-key) columns on indexes: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('firstname','string') + ->addColumn('lastname','string') + ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) + ->create(); + } +} +``` + +PostgreSQL, SQLServer, and SQLite support partial indexes by defining where +clauses for the index: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addColumn('is_verified','boolean') + ->addIndex( + $this->index('email') + ->setName('user_email_verified_idx') + ->setType('unique') + ->setWhere('is_verified = true') + ) + ->create(); + } +} +``` + +PostgreSQL can create indexes concurrently which avoids taking disruptive locks +during index creation: + +``` php +table('users'); + $table->addColumn('email', 'string') + ->addIndex( + $this->index('email') + ->setName('user_email_unique_idx') + ->setType('unique') + ->setConcurrently(true) + ) + ->create(); + } +} +``` + +PostgreSQL adapters also supports Generalized Inverted Index `gin` indexes: + +``` php +table('users'); + $table->addColumn('address', 'string') + ->addIndex('address', ['type' => 'gin']) + ->create(); + } +} +``` + +Removing indexes is as easy as calling the `removeIndex()` method. You must +call this method for each index: + +``` php +table('users'); + $table->removeIndex(['email']) + ->save(); + + // alternatively, you can delete an index by its name, ie: + $table->removeIndexByName('idx_users_email') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +::: info Added in version 4.6.0 +`Index::setWhere()`, and `Index::setConcurrently()` were added. +::: + +### Working With Foreign Keys + +Migrations has support for creating foreign key constraints on your database tables. +Let's add a foreign key to an example table: + +``` php +table('tags'); + $table->addColumn('tag_name', 'string') + ->save(); + + $refTable = $this->table('tag_relationships'); + $refTable->addColumn('tag_id', 'integer', ['null' => true]) + ->addForeignKey( + 'tag_id', + 'tags', + 'id', + ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION'], + ) + ->save(); + + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +The 'delete' and 'update' options allow you to define the `ON UPDATE` and `ON DELETE` behavior. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and +'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable +with the option `['null' => true]`. + +Foreign keys can be defined with arrays of columns to build constraints between +tables with composite keys: + +``` php +table('follower_events'); + $table->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') + ->addForeignKey( + ['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + [ + 'delete'=> 'NO_ACTION', + 'update'=> 'NO_ACTION', + 'constraint' => 'user_follower_id', + ] + ) + ->save(); + } +} +``` + +The options parameter of `addForeignKey()` supports the following options: + +| Option | Description | +|------------|--------------------------------------------------------| +| update | set an action to be triggered when the row is updated | +| delete | set an action to be triggered when the row is deleted | +| constraint | set a name to be used by foreign key constraint | +| deferrable | define deferred constraint application (postgres only) | + +Using the `foreignKey()` method provides a fluent builder to define a foreign +key: + +``` php +table('articles'); + $table->addForeignKey( + $this->foreignKey() + ->setColumns('user_id') + ->setReferencedTable('users') + ->setReferencedColumns('user_id') + ->setDelete(ForeignKey::CASCADE) + ->setName('article_user_fk') + ) + ->save(); + } +} +``` + +::: info Added in version 4.6.0 +The `foreignKey` method was added. +::: + +We can also easily check if a foreign key exists: + +``` php +table('tag_relationships'); + $exists = $table->hasForeignKey('tag_id'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +Finally, to delete a foreign key, use the `dropForeignKey` method. + +Note that like other methods in the `Table` class, `dropForeignKey` also +needs `save()` to be called at the end in order to be executed. This allows +Migrations to intelligently plan migrations when more than one table is +involved: + +``` php +table('tag_relationships'); + $table->dropForeignKey('tag_id')->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Working With Check Constraints + +::: info Added in version 5.0.0 +Check constraints were added in 5.0.0. +::: + +Check constraints allow you to enforce data validation rules at the database level. +They are particularly useful for ensuring data integrity across your application. + +> [!NOTE] +> Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite. +> SQL Server support is planned for a future release. + +#### Adding a Check Constraint + +You can add a check constraint to a table using the `addCheckConstraint()` method: + +``` php +table('products'); + $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) + ->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('products'); + $table->dropCheckConstraint('price_positive') + ->save(); + } +} +``` + +The first argument is the constraint name, and the second is the SQL expression +that defines the constraint. The expression should evaluate to a boolean value. + +#### Using the CheckConstraint Fluent Builder + +For more complex scenarios, you can use the `checkConstraint()` method to get +a fluent builder: + +``` php +table('users'); + $table->addColumn('age', 'integer') + ->addColumn('status', 'string', ['limit' => 20]) + ->addCheckConstraint( + $this->checkConstraint() + ->setName('age_valid') + ->setExpression('age >= 18 AND age <= 120') + ) + ->addCheckConstraint( + $this->checkConstraint() + ->setName('status_valid') + ->setExpression("status IN ('active', 'inactive', 'pending')") + ) + ->save(); + } +} +``` + +#### Auto-Generated Constraint Names + +If you don't specify a constraint name, one will be automatically generated based +on the table name and expression hash: + +``` php +table('inventory'); + $table->addColumn('quantity', 'integer') + // Name will be auto-generated like 'inventory_chk_a1b2c3d4' + ->addCheckConstraint( + $this->checkConstraint() + ->setExpression('quantity >= 0') + ) + ->save(); + } +} +``` + +#### Complex Check Constraints + +Check constraints can reference multiple columns and use complex SQL expressions: + +``` php +table('date_ranges'); + $table->addColumn('start_date', 'date') + ->addColumn('end_date', 'date') + ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2]) + ->addCheckConstraint('valid_date_range', 'end_date >= start_date') + ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100') + ->save(); + } +} +``` + +#### Checking if a Check Constraint Exists + +You can verify if a check constraint exists using the `hasCheckConstraint()` method: + +``` php +table('products'); + $exists = $table->hasCheckConstraint('price_positive'); + if ($exists) { + // do something + } else { + $table->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } + } +} +``` + +#### Dropping a Check Constraint + +To remove a check constraint, use the `dropCheckConstraint()` method with the +constraint name: + +``` php +table('products'); + $table->dropCheckConstraint('price_positive') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('products'); + $table->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } +} +``` + +> [!NOTE] +> Like other table operations, `dropCheckConstraint()` requires `save()` +> to be called to execute the change. + +#### Database-Specific Behavior + +**MySQL (8.0.16+)** + +Check constraints are fully supported. MySQL stores constraint metadata in the +`INFORMATION_SCHEMA.CHECK_CONSTRAINTS` table. + +**PostgreSQL** + +Check constraints are fully supported and stored in the `pg_constraint` catalog. +PostgreSQL allows the most flexible expressions in check constraints. + +**SQLite** + +Check constraints are supported but with some limitations. SQLite does not support +`ALTER TABLE` operations for check constraints, so adding or dropping constraints +requires recreating the entire table. This is handled automatically by the adapter. + +**SQL Server** + +Check constraint support for SQL Server is planned for a future release. + +### Determining Whether a Table Exists + +You can determine whether or not a table exists by using the `hasTable()` +method: + +``` php +hasTable('users'); + if ($exists) { + // do something + } + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Dropping a Table + +Tables can be dropped quite easily using the `drop()` method. It is a +good idea to recreate the table again in the `down()` method. + +Note that like other methods in the `Table` class, `drop` also needs `save()` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved: + +``` php +table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } +} +``` + +### Renaming a Table + +To rename a table access an instance of the Table object then call the +`rename()` method: + +``` php +table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } +} +``` + +### Changing the Primary Key + +To change the primary key on an existing table, use the `changePrimaryKey()` +method. Pass in a column name or array of columns names to include in the +primary key, or `null` to drop the primary key. Note that the mentioned +columns must be added to the table, they will not be added implicitly: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Creating Custom Primary Keys + +You can specify a `autoId` property in the Migration class and set it to +`false`, which will turn off the automatic `id` column creation. You will +need to manually create the column that will be used as a primary key and add +it to the table declaration: + +``` php +table('products'); + $table + ->addColumn('id', 'uuid') + ->addPrimaryKey('id') + ->addColumn('name', 'string') + ->addColumn('description', 'text') + ->create(); + } +} +``` + +The above will create a `CHAR(36)` `id` column that is also the primary key. + +When specifying a custom primary key on the command line, you must note +it as the primary key in the id field, otherwise you may get an error +regarding duplicate id fields, i.e.: + +``` bash +bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified +``` + +All baked migrations and snapshot will use this new way when necessary. + +> [!WARNING] +> Dealing with primary key can only be done on table creation operations. +> This is due to limitations for some database servers the plugin supports. + +### Changing the Table Comment + +To change the comment on an existing table, use the `changeComment()` method. +Pass in a string to set as the new table comment, or `null` to drop the existing comment: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Checking Columns + +`BaseMigration` also provides methods for introspecting the current schema, +allowing you to conditionally make changes to schema, or read data. +Schema is inspected **when the migration is run**. + +### Get a column list + +To retrieve all table columns, simply create a `table` object and call +`getColumns()` method. This method will return an array of Column classes with +basic info. Example below: + +``` php +table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Get a column by name + +To retrieve one table column, simply create a `table` object and call the +`getColumn()` method. This method will return a Column class with basic info +or NULL when the column doesn't exist. Example below: + +``` php +table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Checking whether a column exists + +You can check if a table already has a certain column by using the +`hasColumn()` method: + +``` php +table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } +} + + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Dropping a Table + +Tables can be dropped quite easily using the `drop()` method. It is a +good idea to recreate the table again in the `down()` method. + +Note that like other methods in the `Table` class, `drop` also needs `save()` +to be called at the end in order to be executed. This allows Migrations to intelligently +plan migrations when more than one table is involved: + +``` php +table('users')->drop()->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $users = $this->table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('password_salt', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('first_name', 'string', ['limit' => 30]) + ->addColumn('last_name', 'string', ['limit' => 30]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->save(); + } +} +``` + +### Renaming a Table + +To rename a table access an instance of the Table object then call the +`rename()` method: + +``` php +table('users'); + $table + ->rename('legacy_users') + ->update(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $table = $this->table('legacy_users'); + $table + ->rename('users') + ->update(); + } +} +``` + +### Changing the Primary Key + +To change the primary key on an existing table, use the `changePrimaryKey()` +method. Pass in a column name or array of columns names to include in the +primary key, or `null` to drop the primary key. Note that the mentioned +columns must be added to the table, they will not be added implicitly: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->addColumn('new_id', 'integer', ['null' => false]) + ->changePrimaryKey(['new_id', 'username']) + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +### Creating Custom Primary Keys + +You can specify a `autoId` property in the Migration class and set it to +`false`, which will turn off the automatic `id` column creation. You will +need to manually create the column that will be used as a primary key and add +it to the table declaration: + +``` php +table('products'); + $table + ->addColumn('id', 'uuid') + ->addPrimaryKey('id') + ->addColumn('name', 'string') + ->addColumn('description', 'text') + ->create(); + } +} +``` + +The above will create a `CHAR(36)` `id` column that is also the primary key. + +When specifying a custom primary key on the command line, you must note +it as the primary key in the id field, otherwise you may get an error +regarding duplicate id fields, i.e.: + +``` bash +bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified +``` + +All baked migrations and snapshot will use this new way when necessary. + +> [!WARNING] +> Dealing with primary key can only be done on table creation operations. +> This is due to limitations for some database servers the plugin supports. + +### Changing the Table Comment + +To change the comment on an existing table, use the `changeComment()` method. +Pass in a string to set as the new table comment, or `null` to drop the existing comment: + +``` php +table('users'); + $users + ->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->save(); + + $users + ->changeComment('This is the table with users auth information, password should be encrypted') + ->save(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + + } +} +``` + +## Checking Columns + +`BaseMigration` also provides methods for introspecting the current schema, +allowing you to conditionally make changes to schema, or read data. +Schema is inspected **when the migration is run**. + +### Get a column list + +To retrieve all table columns, simply create a `table` object and call +`getColumns()` method. This method will return an array of Column classes with +basic info. Example below: + +``` php +table('users')->getColumns(); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Get a column by name + +To retrieve one table column, simply create a `table` object and call the +`getColumn()` method. This method will return a Column class with basic info +or NULL when the column doesn't exist. Example below: + +``` php +table('users')->getColumn('email'); + ... + } + + /** + * Migrate Down. + */ + public function down(): void + { + ... + } +} +``` + +### Checking whether a column exists + +You can check if a table already has a certain column by using the +`hasColumn()` method: + +``` php +table('user'); + $column = $table->hasColumn('username'); + + if ($column) { + // do something + } + + } +} +``` + +### Changing templates + +See [Custom Seed Migration Templates](seeding#custom-seed-migration-templates) for how to customize the templates +used to generate migrations. + +## Database-Specific Limitations + +While Migrations aims to provide a database-agnostic API, some features have +database-specific limitations or are not available on all platforms. + +### SQL Server + +The following features are not supported on SQL Server: + +**Check Constraints** + +Check constraints are not currently implemented for SQL Server. Attempting to +use `addCheckConstraint()` or `dropCheckConstraint()` will throw a +`BadMethodCallException`. + +**Table Comments** + +SQL Server does not support table comments. Attempting to use `changeComment()` +will throw a `BadMethodCallException`. + +**INSERT IGNORE / insertOrSkip()** + +SQL Server does not support the `INSERT IGNORE` syntax used by `insertOrSkip()`. +This method will throw a `RuntimeException` on SQL Server. Use `insertOrUpdate()` +instead for upsert operations, which uses `MERGE` statements on SQL Server. + +### SQLite + +**Foreign Key Names** + +SQLite does not support named foreign keys. The foreign key constraint name option +is ignored when creating foreign keys on SQLite. + +**Table Comments** + +SQLite does not support table comments directly. Comments are stored as metadata +but not in the database itself. + +**Check Constraint Modifications** + +SQLite does not support `ALTER TABLE` operations for check constraints. Adding or +dropping check constraints requires recreating the entire table, which is handled +automatically by the adapter. + +**Table Partitioning** + +SQLite does not support table partitioning. + +### PostgreSQL + +**KEY Partitioning** + +PostgreSQL does not support MySQL's `KEY` partitioning type. Use `HASH` +partitioning instead for similar distribution behavior. + +### MySQL/MariaDB + +**insertOrUpdate() Conflict Columns** + +For MySQL, the `$conflictColumns` parameter in `insertOrUpdate()` is ignored +because MySQL's `ON DUPLICATE KEY UPDATE` automatically applies to all unique +constraints. PostgreSQL and SQLite require this parameter to be specified. + +**MariaDB GIS/Geometry** + +Some geometry column features may not work correctly on MariaDB due to differences +in GIS implementation compared to MySQL. diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst deleted file mode 100644 index 6da4195d1..000000000 --- a/docs/en/writing-migrations.rst +++ /dev/null @@ -1,2458 +0,0 @@ -Writing Migrations -################## - -Migrations are a declarative API that helps you transform your database. Each migration -is represented by a PHP class in a unique file. It is preferred that you write -your migrations using the Migrations API, but raw SQL is also supported. - -Creating a New Migration -======================== - -Let's start by creating a new migration with ``bake``: - -.. code-block:: bash - - $ bin/cake bake migration - -This will create a new migration in the format -``YYYYMMDDHHMMSS_my_new_migration.php``, where the first 14 characters are -replaced with the current timestamp down to the second. - -If you have specified multiple migration paths, you will be asked to select -which path to create the new migration in. - -Bake will automatically creates a skeleton migration file with a single method:: - - table('user_logins'); - $table->addColumn('user_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - } - } - -When executing this migration, Migrations will create the ``user_logins`` table on -the way up and automatically figure out how to drop the table on the way down. -Please be aware that when a ``change`` method exists, Migrations will -ignore the ``up`` and ``down`` methods. If you need to use these methods it is -recommended to create a separate migration file. - -.. note:: - - When creating or updating tables inside a ``change()`` method you must use - the Table ``create()`` and ``update()`` methods. Migrations cannot automatically - determine whether a ``save()`` call is creating a new table or modifying an - existing one. - -The following actions are reversible when done through the Table API in -Migrations, and will be automatically reversed: - -- Creating a table -- Renaming a table -- Adding a column -- Renaming a column -- Adding an index -- Adding a foreign key -- Adding a check constraint - -If a command cannot be reversed then Migrations will throw an -``IrreversibleMigrationException`` when it's migrating down. If you wish to -use a command that cannot be reversed in the change function, you can use an -if statement with ``$this->isMigratingUp()`` to only run things in the -up or down direction. For example:: - - table('user_logins'); - $table->addColumn('user_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - if ($this->isMigratingUp()) { - $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) - ->save(); - } - } - } - - -The Up Method -============= - -The up method is automatically run by Migrations when you are migrating up and it -detects the given migration hasn't been executed previously. You should use the -up method to transform the database with your intended changes. - -The Down Method -=============== - -The down method is automatically run by Migrations when you are migrating down and -it detects the given migration has been executed in the past. You should use -the down method to reverse/undo the transformations described in the up method. - -The Init Method -=============== - -The ``init()`` method is run by Migrations before the migration methods if it exists. -This can be used for setting common class properties that are then used within -the migration methods. - -The Should Execute Method -========================= - -The ``shouldExecute()`` method is run by Migrations before executing the migration. -This can be used to prevent the migration from being executed at this time. It always -returns true by default. You can override it in your custom ``BaseMigration`` -implementation. - -Working With Tables -=================== - -The Table object enables you to easily manipulate database tables using PHP -code. You can retrieve an instance of the Table object by calling the -``table()`` method from within your database migration:: - - table('tableName'); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -You can then manipulate this table using the methods provided by the Table -object. - -.. _adding-columns: - -Adding Columns -============== - -Column types are specified as strings and can be one of: - -- binary -- boolean -- char -- date -- datetime -- decimal -- float -- double -- smallinteger -- integer -- biginteger -- string -- text -- time -- timestamp -- uuid -- binaryuuid -- nativeuuid - -In addition, the MySQL adapter supports ``enum``, ``set``, ``blob``, -``tinyblob``, ``mediumblob``, ``longblob``, ``bit`` and ``json`` column types -(``json`` in MySQL 5.7 and above). When providing a limit value and using -``binary``, ``varbinary`` or ``blob`` and its subtypes, the retained column type -will be based on required length (see `Limit Option and MySQL`_ for details). - -With most adapters, the ``uuid`` and ``nativeuuid`` column types are aliases, -however with the MySQL adapter + MariaDB, the ``nativeuuid`` type maps to -a native uuid column instead of ``CHAR(36)`` like ``uuid`` does. - -In addition, the Postgres adapter supports ``interval``, ``json``, ``jsonb``, -``uuid``, ``cidr``, ``inet`` and ``macaddr`` column types (PostgreSQL 9.3 and -above). - -Valid Column Options --------------------- - -The following are valid column options: - -For any column type: - -======= =========== -Option Description -======= =========== -limit set maximum length for strings, also hints column types in adapters (see note below) -length alias for ``limit`` -default set default value or action -null allow ``NULL`` values, defaults to ``true`` (setting ``identity`` will override default to ``false``) -after specify the column that a new column should be placed after, or use ``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` to place the column at the start of the table *(only applies to MySQL)* -comment set a text comment on the column -======= =========== - -For ``decimal`` and ``float`` columns: - -========= =========== -Option Description -========= =========== -precision total number of digits (e.g., 10 in ``DECIMAL(10,2)``) -scale number of digits after the decimal point (e.g., 2 in ``DECIMAL(10,2)``) -signed enable or disable the ``unsigned`` option *(only applies to MySQL)* -========= =========== - -.. note:: - - **Precision and Scale Terminology** - - Migrations follows the SQL standard where ``precision`` represents the total number of digits, - and ``scale`` represents digits after the decimal point. For example, to create ``DECIMAL(10,2)`` - (10 total digits with 2 decimal places): - - .. code-block:: php - - $table->addColumn('price', 'decimal', [ - 'precision' => 10, // Total digits - 'scale' => 2, // Decimal places - ]); - - This differs from CakePHP's TableSchema which uses ``length`` for total digits and - ``precision`` for decimal places. The migration adapter handles this conversion automatically. - -For ``enum`` and ``set`` columns: - -========= =========== -Option Description -========= =========== -values Can be a comma separated list or an array of values -========= =========== - -For ``smallinteger``, ``integer`` and ``biginteger`` columns: - -======== =========== -Option Description -======== =========== -identity enable or disable automatic incrementing (if enabled, will set ``null: false`` if ``null`` option is not set) -signed enable or disable the ``unsigned`` option *(only applies to MySQL)* -======== - -For Postgres, when using ``identity``, it will utilize the ``serial`` type -appropriate for the integer size, so that ``smallinteger`` will give you -``smallserial``, ``integer`` gives ``serial``, and ``biginteger`` gives -``bigserial``. - -For ``date`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_DATE``) -======== =========== - -For ``time`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_TIME``) -timezone enable or disable the ``with time zone`` option *(only applies to Postgres)* -======== =========== - -For ``datetime`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_TIMESTAMP``) -timezone enable or disable the ``with time zone`` option *(only applies to Postgres)* -======== =========== - -For ``timestamp`` columns: - -======== =========== -Option Description -======== =========== -default set default value (use with ``CURRENT_TIMESTAMP``) -update set an action to be triggered when the row is updated (use with ``CURRENT_TIMESTAMP``) *(only applies to MySQL)* -timezone enable or disable the ``with time zone`` option for ``time`` and ``timestamp`` columns *(only applies to Postgres)* -======== =========== - -You can add ``created`` and ``updated`` timestamps to a table using the -``addTimestamps()`` method. This method accepts three arguments, where the first -two allow setting alternative names for the columns while the third argument -allows you to enable the ``timezone`` option for the columns. The defaults for -these arguments are ``created``, ``updated``, and ``false`` respectively. For -the first and second argument, if you provide ``null``, then the default name -will be used, and if you provide ``false``, then that column will not be -created. Please note that attempting to set both to ``false`` will throw -a ``\RuntimeException``. Additionally, you can use the -``addTimestampsWithTimezone()`` method, which is an alias to ``addTimestamps()`` -that will always set the third argument to ``true`` (see examples below). The -``created`` column will have a default set to ``CURRENT_TIMESTAMP``. For MySQL -only, ``updated`` column will have update set to -``CURRENT_TIMESTAMP``:: - - table('users')->addTimestamps()->create(); - // Use defaults (with timezones) - $table = $this->table('users')->addTimestampsWithTimezone()->create(); - - // Override the 'created' column name with 'recorded_at'. - $table = $this->table('books')->addTimestamps('recorded_at')->create(); - - // Override the 'updated' column name with 'amended_at', preserving timezones. - // The two lines below do the same, the second one is simply cleaner. - $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); - $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); - - // Only add the created column to the table - $table = $this->table('books')->addTimestamps(null, false); - // Only add the updated column to the table - $table = $this->table('users')->addTimestamps(false); - // Note, setting both false will throw a \RuntimeError - } - } - -For ``boolean`` columns: - -======== =========== -Option Description -======== =========== -signed enable or disable the ``unsigned`` option *(only applies to MySQL)* -======== =========== - -For ``string`` and ``text`` columns: - -========= =========== -Option Description -========= =========== -collation set collation that differs from table defaults *(only applies to MySQL)* -encoding set character set that differs from table defaults *(only applies to MySQL)* -========= =========== - -Limit Option and MySQL ----------------------- - -When using the MySQL adapter, there are a couple things to consider when working with limits: - -- When using a ``string`` primary key or index on MySQL 5.7 or below, or the - MyISAM storage engine, and the default charset of ``utf8mb4_unicode_ci``, you - must specify a limit less than or equal to 191, or use a different charset. -- Additional hinting of database column type can be made for ``integer``, - ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using - ``limit`` with one the following options will modify the column type - accordingly: - -============ ============== -Limit Column Type -============ ============== -BLOB_TINY TINYBLOB -BLOB_REGULAR BLOB -BLOB_MEDIUM MEDIUMBLOB -BLOB_LONG LONGBLOB -TEXT_TINY TINYTEXT -TEXT_REGULAR TEXT -TEXT_MEDIUM MEDIUMTEXT -TEXT_LONG LONGTEXT -INT_TINY TINYINT -INT_SMALL SMALLINT -INT_MEDIUM MEDIUMINT -INT_REGULAR INT -INT_BIG BIGINT -============ ============== - -For ``binary`` or ``varbinary`` types, if limit is set greater than allowed 255 -bytes, the type will be changed to the best matching blob type given the -length:: - - table('cart_items'); - $table->addColumn('user_id', 'integer') - ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) - ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) - ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) - ->create(); - -Default values with expressions -------------------------------- - -If you need to set a default to an expression, you can use a ``Literal`` to have -the column's default value used without any quoting or escaping. This is helpful -when you want to use a function as a default value:: - - use Migrations\BaseMigration; - use Migrations\Db\Literal; - - class AddSomeColumns extends BaseMigration - { - public function change(): void - { - $this->table('users') - ->addColumn('uniqid', 'uuid', [ - 'default' => Literal::from('uuid_generate_v4()') - ]) - ->create(); - } - } - -.. _creating-a-table:: - -Creating a Table ----------------- - -Creating a table is really easy using the Table object. Let's create a table to -store a collection of users:: - - table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->create(); - } - } - -Columns are added using the ``addColumn()`` method. We create a unique index -for both the username and email columns using the ``addIndex()`` method. -Finally calling ``create()`` commits the changes to the database. - -.. note:: - - Migrations automatically creates an auto-incrementing primary key column called ``id`` for every - table. - -The ``id`` option sets the name of the automatically created identity field, -while the ``primary_key`` option selects the field or fields used for primary -key. ``id`` will always override the ``primary_key`` option unless it's set to -false. If you don't need a primary key set ``id`` to false without specifying -a ``primary_key``, and no primary key will be created. - -To specify an alternate primary key, you can specify the ``primary_key`` option -when accessing the Table object. Let's disable the automatic ``id`` column and -create a primary key using two columns instead:: - - table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); - $table->addColumn('user_id', 'integer') - ->addColumn('follower_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - } - } - -Setting a single ``primary_key`` doesn't enable the ``AUTO_INCREMENT`` option. -To simply change the name of the primary key, we need to override the default ``id`` field name:: - - table('followers', ['id' => 'user_id']); - $table->addColumn('follower_id', 'integer') - ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) - ->create(); - } - } - -In addition, the MySQL adapter supports following options: - -========== ================ =========== -Option Platform Description -========== ================ =========== -comment MySQL, Postgres set a text comment on the table -collation MySQL, SqlServer set the table collation *(defaults to database collation)* -row_format MySQL set the table row format -engine MySQL define table engine *(defaults to ``InnoDB``)* -signed MySQL whether the primary key is ``signed`` *(defaults to ``true``)* -limit MySQL set the maximum length for the primary key -========== ================ =========== - -By default, the primary key is ``signed``. -To set it to be unsigned, pass the ``signed`` option with a ``false`` -value, or enable the ``Migrations.unsigned_primary_keys`` and -``Migrations.unsigned_ints`` feature flags (see :ref:`feature-flags`). -Both flags should be used together so that foreign key columns match -the primary keys they reference:: - - table('followers', ['signed' => false]); - $table->addColumn('follower_id', 'integer') - ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) - ->create(); - } - } - -If you need to create a table with a different collation than the database, -use:: - - table('categories', [ - 'collation' => 'latin1_german1_ci' - ]) - ->addColumn('title', 'string') - ->create(); - } - } - -Note however this can only be done on table creation : there is currently no way -of adding a column to an existing table with a different collation than the -table or the database. Only ``MySQL`` and ``SqlServer`` supports this -configuration key for the time being. - -To view available column types and options, see :ref:`adding-columns` for details. - -MySQL ALTER TABLE Options -------------------------- - -.. versionadded:: 5.0.0 - ``ALGORITHM`` and ``LOCK`` options were added in 5.0.0. - -When modifying tables in MySQL, you can control how the ALTER TABLE operation is -performed using the ``algorithm`` and ``lock`` options. This is useful for performing -zero-downtime schema changes on large tables in production environments. - -.. code-block:: php - - table('large_table'); - $table->addIndex(['status'], [ - 'name' => 'idx_status', - ]); - $table->update([ - 'algorithm' => 'INPLACE', - 'lock' => 'NONE', - ]); - } - } - -Available ``algorithm`` values: - -============ =========== -Algorithm Description -============ =========== -DEFAULT Let MySQL choose the algorithm (default behavior) -INPLACE Modify the table in place without copying data (when possible) -COPY Create a copy of the table with the changes (legacy method) -INSTANT Only modify metadata, no table rebuild (MySQL 8.0+, limited operations) -============ =========== - -Available ``lock`` values: - -========= =========== -Lock Description -========= =========== -DEFAULT Use minimal locking for the algorithm (default behavior) -NONE Allow concurrent reads and writes during the operation -SHARED Allow concurrent reads but block writes -EXCLUSIVE Block all reads and writes during the operation -========= =========== - -.. note:: - - Not all operations support all algorithm/lock combinations. MySQL will raise - an error if the requested combination is not possible for the operation. - The ``INSTANT`` algorithm is only available in MySQL 8.0+ and only for specific - operations like adding columns at the end of a table. - -.. warning:: - - Using ``ALGORITHM=INPLACE, LOCK=NONE`` does not guarantee zero-downtime for - all operations. Some operations may still require a table copy or exclusive lock. - Always test schema changes on a staging environment first. - -Table Partitioning ------------------- - -Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning helps -manage large tables by splitting them into smaller, more manageable pieces. - -.. note:: - - Partition columns must be included in the primary key for MySQL. SQLite does - not support partitioning. MySQL's ``RANGE`` and ``LIST`` types only work with - integer columns - use ``RANGE COLUMNS`` and ``LIST COLUMNS`` for DATE/STRING columns. - -RANGE Partitioning -~~~~~~~~~~~~~~~~~~ - -RANGE partitioning is useful when you want to partition by numeric ranges. For MySQL, -use ``TYPE_RANGE`` with integer columns or expressions, and ``TYPE_RANGE_COLUMNS`` for -DATE/DATETIME/STRING columns:: - - table('orders', [ - 'id' => false, - 'primary_key' => ['id', 'order_date'], - ]); - $table->addColumn('id', 'integer', ['identity' => true]) - ->addColumn('order_date', 'date') - ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2]) - ->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date') - ->addPartition('p2022', '2023-01-01') - ->addPartition('p2023', '2024-01-01') - ->addPartition('p2024', '2025-01-01') - ->addPartition('pmax', 'MAXVALUE') - ->create(); - } - } - -LIST Partitioning -~~~~~~~~~~~~~~~~~ - -LIST partitioning is useful when you want to partition by discrete values. For MySQL, -use ``TYPE_LIST`` with integer columns and ``TYPE_LIST_COLUMNS`` for STRING columns:: - - table('customers', [ - 'id' => false, - 'primary_key' => ['id', 'region'], - ]); - $table->addColumn('id', 'integer', ['identity' => true]) - ->addColumn('region', 'string', ['limit' => 20]) - ->addColumn('name', 'string') - ->partitionBy(Partition::TYPE_LIST_COLUMNS, 'region') - ->addPartition('p_americas', ['US', 'CA', 'MX', 'BR']) - ->addPartition('p_europe', ['UK', 'DE', 'FR', 'IT']) - ->addPartition('p_asia', ['JP', 'CN', 'IN', 'KR']) - ->create(); - } - } - -HASH Partitioning -~~~~~~~~~~~~~~~~~ - -HASH partitioning distributes data evenly across a specified number of partitions:: - - table('sessions'); - $table->addColumn('user_id', 'integer') - ->addColumn('data', 'text') - ->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8]) - ->create(); - } - } - -KEY Partitioning (MySQL only) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -KEY partitioning is similar to HASH but uses MySQL's internal hashing function:: - - table('cache', [ - 'id' => false, - 'primary_key' => ['cache_key'], - ]); - $table->addColumn('cache_key', 'string', ['limit' => 255]) - ->addColumn('value', 'binary') - ->partitionBy(Partition::TYPE_KEY, 'cache_key', ['count' => 16]) - ->create(); - } - } - -Partitioning with Expressions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can partition by expressions using the ``Literal`` class:: - - table('events', [ - 'id' => false, - 'primary_key' => ['id', 'created_at'], - ]); - $table->addColumn('id', 'integer', ['identity' => true]) - ->addColumn('created_at', 'datetime') - ->partitionBy(Partition::TYPE_RANGE, Literal::from('YEAR(created_at)')) - ->addPartition('p2022', 2023) - ->addPartition('p2023', 2024) - ->addPartition('pmax', 'MAXVALUE') - ->create(); - } - } - -Modifying Partitions on Existing Tables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can add or drop partitions on existing partitioned tables:: - - table('orders') - ->addPartitionToExisting('p2025', '2026-01-01') - ->update(); - } - - public function down(): void - { - // Drop the partition - $this->table('orders') - ->dropPartition('p2025') - ->update(); - } - } - -Saving Changes --------------- - -When working with the Table object, Migrations stores certain operations in a -pending changes cache. Once you have made the changes you want to the table, -you must save them. To perform this operation, Migrations provides three methods, -``create()``, ``update()``, and ``save()``. ``create()`` will first create -the table and then run the pending changes. ``update()`` will just run the -pending changes, and should be used when the table already exists. ``save()`` -is a helper function that checks first if the table exists and if it does not -will run ``create()``, else it will run ``update()``. - -As stated above, when using the ``change()`` migration method, you should always -use ``create()`` or ``update()``, and never ``save()`` as otherwise migrating -and rolling back may result in different states, due to ``save()`` calling -``create()`` when running migrate and then ``update()`` on rollback. When -using the ``up()``/``down()`` methods, it is safe to use either ``save()`` or -the more explicit methods. - -When in doubt with working with tables, it is always recommended to call -the appropriate function and commit any pending changes to the database. - - -Renaming a Column ------------------ - -To rename a column, access an instance of the Table object then call the -``renameColumn()`` method:: - - table('users'); - $table->renameColumn('bio', 'biography') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('users'); - $table->renameColumn('biography', 'bio') - ->save(); - } - } - -Adding a Column After Another Column ------------------------------------- - -When adding a column with the MySQL adapter, you can dictate its position using -the ``after`` option, where its value is the name of the column to position it -after:: - - table('users'); - $table->addColumn('city', 'string', ['after' => 'email']) - ->update(); - } - } - -This would create the new column ``city`` and position it after the ``email`` -column. The ``\Migrations\Db\Adapter\MysqlAdapter::FIRST`` constant can be used -to specify that the new column should be created as the first column in that -table. - -Dropping a Column ------------------ - -To drop a column, use the ``removeColumn()`` method:: - - table('users'); - $table->removeColumn('short_name') - ->save(); - } - } - - -Specifying a Column Limit -------------------------- - -You can limit the maximum length of a column by using the ``limit`` option:: - - table('tags'); - $table->addColumn('short_name', 'string', ['limit' => 30]) - ->update(); - } - } - -Changing Column Attributes --------------------------- - -There are two methods for modifying existing columns: - -Updating Columns (Recommended) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To modify specific column attributes while preserving others, use the ``updateColumn()`` method. -This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.:: - - table('users'); - // Make email nullable, preserving all other attributes - $users->updateColumn('email', null, ['null' => true]) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->updateColumn('email', null, ['null' => false]) - ->save(); - } - } - -You can pass ``null`` as the column type to preserve the existing type, or specify a new type:: - - // Preserve type and other attributes, only change nullability - $table->updateColumn('email', null, ['null' => true]); - - // Change type to biginteger, preserve default and other attributes - $table->updateColumn('user_id', 'biginteger'); - - // Change default value, preserve everything else - $table->updateColumn('status', null, ['default' => 'active']); - -The following attributes are automatically preserved by ``updateColumn()``: - -- Default values -- NULL/NOT NULL constraint -- Column limit/length -- Decimal scale/precision -- Comments -- Signed/unsigned (for numeric types) -- Collation and encoding -- Enum/set values - -Changing Columns (Traditional) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To completely replace a column definition, use the ``changeColumn()`` method. -This method requires you to specify all desired column attributes. -See :ref:`valid-column-types` and `Valid Column Options`_ for allowed values:: - - table('users'); - // Must specify all attributes - $users->changeColumn('email', 'string', [ - 'limit' => 255, - 'null' => true, - 'default' => null, - ]) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -You can enable attribute preservation with ``changeColumn()`` by passing -``'preserveUnspecified' => true`` in the options:: - - $table->changeColumn('email', 'string', [ - 'null' => true, - 'preserveUnspecified' => true, - ]); - -.. note:: - - For most use cases, ``updateColumn()`` is recommended as it is safer and requires - less code. Use ``changeColumn()`` when you need to completely redefine a column - or when working with legacy code that expects the traditional behavior. - -Working With Indexes --------------------- - -To add an index to a table you can simply call the ``addIndex()`` method on the -table object:: - - table('users'); - $table->addColumn('city', 'string') - ->addIndex(['city']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -By default Migrations instructs the database adapter to create a simple index. We -can pass an additional parameter ``unique`` to the ``addIndex()`` method to -specify a unique index. We can also explicitly specify a name for the index -using the ``name`` parameter, the index columns sort order can also be specified using -the ``order`` parameter. The order parameter takes an array of column names and sort order key/value pairs:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addIndex(['email', 'username'], [ - 'unique' => true, - 'name' => 'idx_users_email', - 'order' => ['email' => 'DESC', 'username' => 'ASC']] - ) - ->save(); - } - } - -As of 4.6.0, you can use ``BaseMigration::index()`` to get a fluent builder to -define indexes:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addIndex( - $this->index(['email', 'username']) - ->setType('unique') - ->setName('idx_users_email') - ->setOrder(['email' => 'DESC', 'username' => 'ASC']) - ) - ->save(); - } - } - - -The MySQL adapter also supports ``fulltext`` indexes. If you are using a version before 5.6 you must -ensure the table uses the ``MyISAM`` engine:: - - table('users', ['engine' => 'MyISAM']); - $table->addColumn('email', 'string') - ->addIndex('email', ['type' => 'fulltext']) - ->create(); - } - } - -MySQL adapter supports setting the index length defined by limit option. -When you are using a multi-column index, you are able to define each column index length. -The single column index can define its index length with or without defining column name in limit option:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addColumn('user_guid', 'string', ['limit' => 36]) - ->addIndex(['email','username'], ['limit' => ['email' => 5, 'username' => 2]]) - ->addIndex('user_guid', ['limit' => 6]) - ->create(); - } - } - -The SQL Server and PostgreSQL adapters support ``include`` (non-key) columns on indexes:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('firstname','string') - ->addColumn('lastname','string') - ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) - ->create(); - } - } - -PostgreSQL, SQLServer, and SQLite support partial indexes by defining where -clauses for the index:: - - table('users'); - $table->addColumn('email', 'string') - ->addColumn('is_verified','boolean') - ->addIndex( - $this->index('email') - ->setName('user_email_verified_idx') - ->setType('unique') - ->setWhere('is_verified = true') - ) - ->create(); - } - } - -PostgreSQL can create indexes concurrently which avoids taking disruptive locks -during index creation:: - - table('users'); - $table->addColumn('email', 'string') - ->addIndex( - $this->index('email') - ->setName('user_email_unique_idx') - ->setType('unique') - ->setConcurrently(true) - ) - ->create(); - } - } - -PostgreSQL adapters also supports Generalized Inverted Index ``gin`` indexes:: - - table('users'); - $table->addColumn('address', 'string') - ->addIndex('address', ['type' => 'gin']) - ->create(); - } - } - -Removing indexes is as easy as calling the ``removeIndex()`` method. You must -call this method for each index:: - - table('users'); - $table->removeIndex(['email']) - ->save(); - - // alternatively, you can delete an index by its name, ie: - $table->removeIndexByName('idx_users_email') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -.. versionadded:: 4.6.0 - ``Index::setWhere()``, and ``Index::setConcurrently()`` were added. - - -Working With Foreign Keys -------------------------- - -Migrations has support for creating foreign key constraints on your database tables. -Let's add a foreign key to an example table:: - - table('tags'); - $table->addColumn('tag_name', 'string') - ->save(); - - $refTable = $this->table('tag_relationships'); - $refTable->addColumn('tag_id', 'integer', ['null' => true]) - ->addForeignKey( - 'tag_id', - 'tags', - 'id', - ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION'], - ) - ->save(); - - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -The 'delete' and 'update' options allow you to define the ``ON UPDATE`` and ``ON -DELETE`` behavior. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and -'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable -with the option ``['null' => true]``. - -Foreign keys can be defined with arrays of columns to build constraints between -tables with composite keys:: - - table('follower_events'); - $table->addColumn('user_id', 'integer') - ->addColumn('follower_id', 'integer') - ->addColumn('event_id', 'integer') - ->addForeignKey( - ['user_id', 'follower_id'], - 'followers', - ['user_id', 'follower_id'], - [ - 'delete'=> 'NO_ACTION', - 'update'=> 'NO_ACTION', - 'constraint' => 'user_follower_id', - ] - ) - ->save(); - } - } - -The options parameter of ``addForeignKey()`` supports the following options: - -========== =========== -Option Description -========== =========== -update set an action to be triggered when the row is updated -delete set an action to be triggered when the row is deleted -constraint set a name to be used by foreign key constraint -deferrable define deferred constraint application (postgres only) -========== =========== - -Using the ``foreignKey()`` method provides a fluent builder to define a foreign -key:: - - table('articles'); - $table->addForeignKey( - $this->foreignKey() - ->setColumns('user_id') - ->setReferencedTable('users') - ->setReferencedColumns('user_id') - ->setDelete(ForeignKey::CASCADE) - ->setName('article_user_fk') - ) - ->save(); - } - } - -.. versionadded:: 4.6.0 - The ``foreignKey`` method was added. - -We can also easily check if a foreign key exists:: - - table('tag_relationships'); - $exists = $table->hasForeignKey('tag_id'); - if ($exists) { - // do something - } - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Finally, to delete a foreign key, use the ``dropForeignKey`` method. - -Note that like other methods in the ``Table`` class, ``dropForeignKey`` also -needs ``save()`` to be called at the end in order to be executed. This allows -Migrations to intelligently plan migrations when more than one table is -involved:: - - table('tag_relationships'); - $table->dropForeignKey('tag_id')->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Working With Check Constraints -------------------------------- - -.. versionadded:: 5.0.0 - Check constraints were added in 5.0.0. - -Check constraints allow you to enforce data validation rules at the database level. -They are particularly useful for ensuring data integrity across your application. - -.. note:: - - Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite. - SQL Server support is planned for a future release. - -Adding a Check Constraint -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can add a check constraint to a table using the ``addCheckConstraint()`` method:: - - table('products'); - $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) - ->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('products'); - $table->dropCheckConstraint('price_positive') - ->save(); - } - } - -The first argument is the constraint name, and the second is the SQL expression -that defines the constraint. The expression should evaluate to a boolean value. - -Using the CheckConstraint Fluent Builder -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For more complex scenarios, you can use the ``checkConstraint()`` method to get -a fluent builder:: - - table('users'); - $table->addColumn('age', 'integer') - ->addColumn('status', 'string', ['limit' => 20]) - ->addCheckConstraint( - $this->checkConstraint() - ->setName('age_valid') - ->setExpression('age >= 18 AND age <= 120') - ) - ->addCheckConstraint( - $this->checkConstraint() - ->setName('status_valid') - ->setExpression("status IN ('active', 'inactive', 'pending')") - ) - ->save(); - } - } - -Auto-Generated Constraint Names -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you don't specify a constraint name, one will be automatically generated based -on the table name and expression hash:: - - table('inventory'); - $table->addColumn('quantity', 'integer') - // Name will be auto-generated like 'inventory_chk_a1b2c3d4' - ->addCheckConstraint( - $this->checkConstraint() - ->setExpression('quantity >= 0') - ) - ->save(); - } - } - -Complex Check Constraints -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Check constraints can reference multiple columns and use complex SQL expressions:: - - table('date_ranges'); - $table->addColumn('start_date', 'date') - ->addColumn('end_date', 'date') - ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2]) - ->addCheckConstraint('valid_date_range', 'end_date >= start_date') - ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100') - ->save(); - } - } - -Checking if a Check Constraint Exists -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can verify if a check constraint exists using the ``hasCheckConstraint()`` method:: - - table('products'); - $exists = $table->hasCheckConstraint('price_positive'); - if ($exists) { - // do something - } else { - $table->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - } - } - -Dropping a Check Constraint -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To remove a check constraint, use the ``dropCheckConstraint()`` method with the -constraint name:: - - table('products'); - $table->dropCheckConstraint('price_positive') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('products'); - $table->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - } - -.. note:: - - Like other table operations, ``dropCheckConstraint()`` requires ``save()`` - to be called to execute the change. - -Database-Specific Behavior -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**MySQL (8.0.16+)** - -Check constraints are fully supported. MySQL stores constraint metadata in the -``INFORMATION_SCHEMA.CHECK_CONSTRAINTS`` table. - -**PostgreSQL** - -Check constraints are fully supported and stored in the ``pg_constraint`` catalog. -PostgreSQL allows the most flexible expressions in check constraints. - -**SQLite** - -Check constraints are supported but with some limitations. SQLite does not support -``ALTER TABLE`` operations for check constraints, so adding or dropping constraints -requires recreating the entire table. This is handled automatically by the adapter. - -**SQL Server** - -Check constraint support for SQL Server is planned for a future release. - -Determining Whether a Table Exists ----------------------------------- - -You can determine whether or not a table exists by using the ``hasTable()`` -method:: - - hasTable('users'); - if ($exists) { - // do something - } - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Dropping a Table ----------------- - -Tables can be dropped quite easily using the ``drop()`` method. It is a -good idea to recreate the table again in the ``down()`` method. - -Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` -to be called at the end in order to be executed. This allows Migrations to intelligently -plan migrations when more than one table is involved:: - - table('users')->drop()->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->save(); - } - } - -Renaming a Table ----------------- - -To rename a table access an instance of the Table object then call the -``rename()`` method:: - - table('users'); - $table - ->rename('legacy_users') - ->update(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('legacy_users'); - $table - ->rename('users') - ->update(); - } - } - -Changing the Primary Key ------------------------- - -To change the primary key on an existing table, use the ``changePrimaryKey()`` -method. Pass in a column name or array of columns names to include in the -primary key, or ``null`` to drop the primary key. Note that the mentioned -columns must be added to the table, they will not be added implicitly:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->addColumn('new_id', 'integer', ['null' => false]) - ->changePrimaryKey(['new_id', 'username']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Creating Custom Primary Keys ----------------------------- - -You can specify a ``autoId`` property in the Migration class and set it to -``false``, which will turn off the automatic ``id`` column creation. You will -need to manually create the column that will be used as a primary key and add -it to the table declaration:: - - table('products'); - $table - ->addColumn('id', 'uuid') - ->addPrimaryKey('id') - ->addColumn('name', 'string') - ->addColumn('description', 'text') - ->create(); - } - } - -The above will create a ``CHAR(36)`` ``id`` column that is also the primary key. - -When specifying a custom primary key on the command line, you must note -it as the primary key in the id field, otherwise you may get an error -regarding duplicate id fields, i.e.: - -.. code-block:: bash - - bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified - - -All baked migrations and snapshot will use this new way when necessary. - -.. warning:: - - Dealing with primary key can only be done on table creation operations. - This is due to limitations for some database servers the plugin supports. - -Changing the Table Comment --------------------------- - -To change the comment on an existing table, use the ``changeComment()`` method. -Pass in a string to set as the new table comment, or ``null`` to drop the existing comment:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->changeComment('This is the table with users auth information, password should be encrypted') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Checking Columns -================ - -``BaseMigration`` also provides methods for introspecting the current schema, -allowing you to conditionally make changes to schema, or read data. -Schema is inspected **when the migration is run**. - -Get a column list ------------------ - -To retrieve all table columns, simply create a ``table`` object and call -``getColumns()`` method. This method will return an array of Column classes with -basic info. Example below:: - - table('users')->getColumns(); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Get a column by name --------------------- - -To retrieve one table column, simply create a ``table`` object and call the -``getColumn()`` method. This method will return a Column class with basic info -or NULL when the column doesn't exist. Example below:: - - table('users')->getColumn('email'); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Checking whether a column exists --------------------------------- - -You can check if a table already has a certain column by using the -``hasColumn()`` method:: - - table('user'); - $column = $table->hasColumn('username'); - - if ($column) { - // do something - } - - } - } - - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Dropping a Table ----------------- - -Tables can be dropped quite easily using the ``drop()`` method. It is a -good idea to recreate the table again in the ``down()`` method. - -Note that like other methods in the ``Table`` class, ``drop`` also needs ``save()`` -to be called at the end in order to be executed. This allows Migrations to intelligently -plan migrations when more than one table is involved:: - - table('users')->drop()->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->save(); - } - } - -Renaming a Table ----------------- - -To rename a table access an instance of the Table object then call the -``rename()`` method:: - - table('users'); - $table - ->rename('legacy_users') - ->update(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('legacy_users'); - $table - ->rename('users') - ->update(); - } - } - -Changing the Primary Key ------------------------- - -To change the primary key on an existing table, use the ``changePrimaryKey()`` -method. Pass in a column name or array of columns names to include in the -primary key, or ``null`` to drop the primary key. Note that the mentioned -columns must be added to the table, they will not be added implicitly:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->addColumn('new_id', 'integer', ['null' => false]) - ->changePrimaryKey(['new_id', 'username']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Creating Custom Primary Keys ----------------------------- - -You can specify a ``autoId`` property in the Migration class and set it to -``false``, which will turn off the automatic ``id`` column creation. You will -need to manually create the column that will be used as a primary key and add -it to the table declaration:: - - table('products'); - $table - ->addColumn('id', 'uuid') - ->addPrimaryKey('id') - ->addColumn('name', 'string') - ->addColumn('description', 'text') - ->create(); - } - } - -The above will create a ``CHAR(36)`` ``id`` column that is also the primary key. - -When specifying a custom primary key on the command line, you must note -it as the primary key in the id field, otherwise you may get an error -regarding duplicate id fields, i.e.: - -.. code-block:: bash - - bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified - - -All baked migrations and snapshot will use this new way when necessary. - -.. warning:: - - Dealing with primary key can only be done on table creation operations. - This is due to limitations for some database servers the plugin supports. - -Changing the Table Comment --------------------------- - -To change the comment on an existing table, use the ``changeComment()`` method. -Pass in a string to set as the new table comment, or ``null`` to drop the existing comment:: - - table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->changeComment('This is the table with users auth information, password should be encrypted') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } - } - -Checking Columns -================ - -``BaseMigration`` also provides methods for introspecting the current schema, -allowing you to conditionally make changes to schema, or read data. -Schema is inspected **when the migration is run**. - -Get a column list ------------------ - -To retrieve all table columns, simply create a ``table`` object and call -``getColumns()`` method. This method will return an array of Column classes with -basic info. Example below:: - - table('users')->getColumns(); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Get a column by name --------------------- - -To retrieve one table column, simply create a ``table`` object and call the -``getColumn()`` method. This method will return a Column class with basic info -or NULL when the column doesn't exist. Example below:: - - table('users')->getColumn('email'); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } - } - -Checking whether a column exists --------------------------------- - -You can check if a table already has a certain column by using the -``hasColumn()`` method:: - - table('user'); - $column = $table->hasColumn('username'); - - if ($column) { - // do something - } - - } - } - - -Changing templates ------------------- - -See :ref:`custom-seed-migration-templates` for how to customize the templates -used to generate migrations. - -Database-Specific Limitations -============================= - -While Migrations aims to provide a database-agnostic API, some features have -database-specific limitations or are not available on all platforms. - -SQL Server ----------- - -The following features are not supported on SQL Server: - -**Check Constraints** - -Check constraints are not currently implemented for SQL Server. Attempting to -use ``addCheckConstraint()`` or ``dropCheckConstraint()`` will throw a -``BadMethodCallException``. - -**Table Comments** - -SQL Server does not support table comments. Attempting to use ``changeComment()`` -will throw a ``BadMethodCallException``. - -**INSERT IGNORE / insertOrSkip()** - -SQL Server does not support the ``INSERT IGNORE`` syntax used by ``insertOrSkip()``. -This method will throw a ``RuntimeException`` on SQL Server. Use ``insertOrUpdate()`` -instead for upsert operations, which uses ``MERGE`` statements on SQL Server. - -SQLite ------- - -**Foreign Key Names** - -SQLite does not support named foreign keys. The foreign key constraint name option -is ignored when creating foreign keys on SQLite. - -**Table Comments** - -SQLite does not support table comments directly. Comments are stored as metadata -but not in the database itself. - -**Check Constraint Modifications** - -SQLite does not support ``ALTER TABLE`` operations for check constraints. Adding or -dropping check constraints requires recreating the entire table, which is handled -automatically by the adapter. - -**Table Partitioning** - -SQLite does not support table partitioning. - -PostgreSQL ----------- - -**KEY Partitioning** - -PostgreSQL does not support MySQL's ``KEY`` partitioning type. Use ``HASH`` -partitioning instead for similar distribution behavior. - -MySQL/MariaDB -------------- - -**insertOrUpdate() Conflict Columns** - -For MySQL, the ``$conflictColumns`` parameter in ``insertOrUpdate()`` is ignored -because MySQL's ``ON DUPLICATE KEY UPDATE`` automatically applies to all unique -constraints. PostgreSQL and SQLite require this parameter to be specified. - -**MariaDB GIS/Geometry** - -Some geometry column features may not work correctly on MariaDB due to differences -in GIS implementation compared to MySQL. diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 000000000..f22cc50b3 --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2104 @@ +{ + "name": "docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@cakephp/docs-skeleton": "github:cakephp/docs-skeleton#node-package", + "vitepress": "^2.0.0-alpha.16" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cakephp/docs-skeleton": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#beb4b50177ce8fd81d5113d2e3fc49b107ddf2f2", + "bin": { + "cakedocs": "bin/cakedocs.js" + }, + "peerDependencies": { + "vitepress": "^2.0.0-alpha.15" + } + }, + "node_modules/@docsearch/css": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.2.tgz", + "integrity": "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==", + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.2.tgz", + "integrity": "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==", + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.2.tgz", + "integrity": "sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.75", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.75.tgz", + "integrity": "sha512-KvcCUbvcBWb0sbqLIxHoY8z5/piXY08wcY9gfMhF+ph3AfzGMaSmZFkUY71HSXAljQngXkgs4bdKdekO0HQWvg==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", + "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz", + "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", + "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.31", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", + "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", + "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.31", + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", + "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", + "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", + "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", + "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", + "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", + "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/shared": "3.5.31" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", + "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.31", + "@vue/runtime-core": "3.5.31", + "@vue/shared": "3.5.31", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", + "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "vue": "3.5.31" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", + "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", + "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", + "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.2.1", + "@vueuse/shared": "14.2.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", + "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", + "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-trap": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.0.1.tgz", + "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==", + "license": "MIT", + "peer": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", + "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", + "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.23.0", + "@shikijs/engine-javascript": "3.23.0", + "@shikijs/engine-oniguruma": "3.23.0", + "@shikijs/langs": "3.23.0", + "@shikijs/themes": "3.23.0", + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", + "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@docsearch/css": "^4.5.3", + "@docsearch/js": "^4.5.3", + "@docsearch/sidepanel-js": "^4.5.3", + "@iconify-json/simple-icons": "^1.2.69", + "@shikijs/core": "^3.22.0", + "@shikijs/transformers": "^3.22.0", + "@shikijs/types": "^3.22.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.4", + "@vue/devtools-api": "^8.0.5", + "@vue/shared": "^3.5.27", + "@vueuse/core": "^14.2.0", + "@vueuse/integrations": "^14.2.0", + "focus-trap": "^8.0.0", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^3.22.0", + "vite": "^7.3.1", + "vue": "^3.5.27" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "oxc-minify": "*", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "oxc-minify": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.31", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", + "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.31", + "@vue/compiler-sfc": "3.5.31", + "@vue/runtime-dom": "3.5.31", + "@vue/server-renderer": "3.5.31", + "@vue/shared": "3.5.31" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..fb3f3a14b --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "docs", + "version": "1.0.0", + "description": "", + "main": "config.js", + "scripts": { + "docs:dev": "vitepress dev", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@cakephp/docs-skeleton": "github:cakephp/docs-skeleton#node-package", + "vitepress": "^2.0.0-alpha.16" + } +} From 4ccc5f4a01c7b3ce673ea63b455767e07f9c9805 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 29 Mar 2026 15:52:17 +0200 Subject: [PATCH 23/72] fix base for new docs --- docs/.vitepress/config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index b0ede705f..702f50b55 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -7,7 +7,7 @@ const toc_en = require("./toc_en.json"); const versions = { text: "5.x", items: [ - { text: "5.x (current)", link: "book.cakephp.org/migrations/5.x/", target: '_self' }, + { text: "5.x (current)", link: "book.cakephp.org/migrations/5/", target: '_self' }, { text: "4.x", link: "https://book.cakephp.org/migrations/4/", target: '_self' }, { text: "3.x", link: "https://book.cakephp.org/migrations/3/", target: '_self' }, { text: "2.x", link: "https://book.cakephp.org/migrations/2/", target: '_self' }, @@ -20,12 +20,12 @@ export default { srcDir: 'en', title: 'Migrations plugin', description: 'Migrations - CakePHP migrations Documentation', - base: "/5.x/", + base: '/migrations/5/', rewrites: { "en/:slug*": ":slug*", }, sitemap: { - hostname: "https://book.cakephp.org/migrations/5.x/", + hostname: "https://book.cakephp.org/migrations/5/", }, themeConfig: { socialLinks: [ From eafc8aac16cf0d56aa514a1bc2c9d22a9d102de4 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 29 Mar 2026 15:54:34 +0200 Subject: [PATCH 24/72] fix sidebar having homepage linked --- docs/.vitepress/toc_en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json index b7f94176a..788a3039e 100644 --- a/docs/.vitepress/toc_en.json +++ b/docs/.vitepress/toc_en.json @@ -4,7 +4,6 @@ "text": "Migrations", "collapsed": false, "items": [ - { "text": "Introduction", "link": "/index" }, { "text": "Writing Migrations", "link": "/writing-migrations" }, { "text": "Using the Query Builder", "link": "/using-the-query-builder" }, { "text": "Executing Queries", "link": "/executing-queries" }, From 7324b1ab413c6f72f788a6baff1eec0f3fb86d06 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Tue, 31 Mar 2026 04:09:59 +0200 Subject: [PATCH 25/72] add rector (#1062) --- composer.json | 3 + phpstan-baseline.neon | 12 + rector.php | 54 +++ src/BaseMigration.php | 35 +- src/BaseSeed.php | 27 +- src/Command/BakeMigrationCommand.php | 5 +- src/Command/BakeMigrationDiffCommand.php | 48 +- src/Command/BakeMigrationSnapshotCommand.php | 5 +- src/Command/BakeSeedCommand.php | 13 +- src/Command/BakeSimpleMigrationCommand.php | 10 +- src/Command/DumpCommand.php | 17 +- src/Command/EntryCommand.php | 6 +- src/Command/MarkMigratedCommand.php | 13 +- src/Command/MigrateCommand.php | 4 +- src/Command/RollbackCommand.php | 4 +- src/Command/SeedCommand.php | 4 +- src/Command/SeedResetCommand.php | 18 +- src/Command/SeedStatusCommand.php | 14 +- src/Command/SeedsEntryCommand.php | 6 +- src/Command/StatusCommand.php | 3 +- src/Command/UpgradeCommand.php | 24 +- src/Config/Config.php | 5 +- src/Config/ConfigInterface.php | 1 + src/Db/Action/Action.php | 3 - src/Db/Action/AddColumn.php | 2 - src/Db/Action/AddForeignKey.php | 2 - src/Db/Action/AddIndex.php | 2 - src/Db/Action/AddPartition.php | 3 - src/Db/Action/ChangeColumn.php | 6 +- src/Db/Action/ChangeComment.php | 2 - src/Db/Action/DropForeignKey.php | 2 - src/Db/Action/DropIndex.php | 2 - src/Db/Action/DropPartition.php | 3 - src/Db/Action/RemoveColumn.php | 2 - src/Db/Action/RenameColumn.php | 4 - src/Db/Action/RenameTable.php | 2 - src/Db/Action/SetPartitioning.php | 3 - src/Db/Adapter/AbstractAdapter.php | 112 ++--- src/Db/Adapter/AdapterFactory.php | 4 +- src/Db/Adapter/AdapterInterface.php | 50 ++ src/Db/Adapter/AdapterWrapper.php | 3 - src/Db/Adapter/MysqlAdapter.php | 89 ++-- src/Db/Adapter/PostgresAdapter.php | 94 ++-- src/Db/Adapter/RecordingAdapter.php | 2 +- src/Db/Adapter/SqliteAdapter.php | 237 +++++----- src/Db/Adapter/SqlserverAdapter.php | 42 +- src/Db/Adapter/TimedOutputAdapter.php | 10 +- src/Db/Expression.php | 4 +- src/Db/Literal.php | 4 +- src/Db/Plan/AlterTable.php | 2 - src/Db/Plan/NewTable.php | 2 - src/Db/Plan/Plan.php | 21 +- src/Db/Plan/Solver/ActionSplitter.php | 7 +- src/Db/Table.php | 60 +-- src/Db/Table/Column.php | 61 +-- src/Db/Table/ForeignKey.php | 6 + src/Db/Table/Partition.php | 11 +- src/Db/Table/TableMetadata.php | 6 - .../PendingMigrationsMiddleware.php | 2 - src/Migration/BuiltinBackend.php | 4 - src/Migration/Environment.php | 19 +- src/Migration/Manager.php | 109 ++--- src/Migration/ManagerFactory.php | 4 +- src/Migrations.php | 2 - src/TestSuite/Migrator.php | 31 +- src/Util/ColumnParser.php | 45 +- src/Util/TableFinder.php | 12 +- src/Util/Util.php | 14 +- src/Util/UtilTrait.php | 4 +- src/View/Helper/MigrationHelper.php | 33 +- .../Command/BakeMigrationCommandTest.php | 48 +- .../Command/BakeMigrationDiffCommandTest.php | 65 +-- .../BakeMigrationSnapshotCommandTest.php | 43 +- .../TestCase/Command/BakeSeedCommandTest.php | 20 +- tests/TestCase/Command/CompletionTest.php | 14 +- tests/TestCase/Command/DumpCommandTest.php | 6 +- tests/TestCase/Command/EntryCommandTest.php | 6 +- tests/TestCase/Command/MarkMigratedTest.php | 16 +- tests/TestCase/Command/MigrateCommandTest.php | 26 +- .../TestCase/Command/RollbackCommandTest.php | 7 +- tests/TestCase/Command/SeedCommandTest.php | 26 +- tests/TestCase/Command/StatusCommandTest.php | 5 +- tests/TestCase/Command/UpgradeCommandTest.php | 16 +- .../Config/ConfigMigrationPathsTest.php | 4 +- tests/TestCase/Config/ConfigSeedPathsTest.php | 6 +- tests/TestCase/Config/ConfigTest.php | 18 +- .../Db/Adapter/AbstractAdapterTest.php | 17 +- .../Db/Adapter/AdapterFactoryTest.php | 40 +- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 422 +++++++++-------- .../Db/Adapter/PostgresAdapterTest.php | 323 ++++++------- .../Db/Adapter/RecordingAdapterTest.php | 37 +- .../TestCase/Db/Adapter/SqliteAdapterTest.php | 428 +++++++++--------- .../Db/Adapter/SqlserverAdapterTest.php | 176 ++++--- .../UnifiedMigrationsTableStorageTest.php | 16 +- tests/TestCase/Db/LiteralTest.php | 6 +- tests/TestCase/Db/Table/ColumnTest.php | 8 +- tests/TestCase/Db/Table/ForeignKeyTest.php | 29 +- tests/TestCase/Db/Table/IndexTest.php | 2 +- tests/TestCase/Db/Table/TableTest.php | 79 ++-- .../PendingMigrationsMiddlewareTest.php | 14 +- tests/TestCase/Migration/EnvironmentTest.php | 46 +- .../TestCase/Migration/ManagerFactoryTest.php | 8 +- tests/TestCase/Migration/ManagerTest.php | 88 ++-- tests/TestCase/MigrationsTest.php | 49 +- tests/TestCase/TestCase.php | 14 +- tests/TestCase/TestSuite/MigratorTest.php | 6 +- tests/TestCase/Util/ColumnParserTest.php | 32 +- tests/TestCase/Util/UtilTest.php | 16 +- .../20120111235330_test_migration.php | 4 +- .../View/Helper/MigrationHelperTest.php | 20 +- tests/bootstrap.php | 6 +- .../test_app/App/Http/TestRequestHandler.php | 5 +- .../Migrations/20211001000000_migrator.php | 2 +- .../Migrations2/20211002000000_migrator2.php | 2 +- ...0190928205056_first_fk_index_migration.php | 4 +- ...190928205060_second_fk_index_migration.php | 2 +- ...13232502_create_drop_fk_initial_schema.php | 2 +- .../20121223011815_add_regression_drop_fk.php | 2 +- .../20121223011816_change_fk_regression.php | 2 +- ...0121223011817_change_column_regression.php | 2 +- ...20190928205056_first_drop_fk_migration.php | 2 +- ...0190928205060_second_drop_fk_migration.php | 2 +- ...0120111235330_duplicate_migration_name.php | 4 +- ...0120111235331_duplicate_migration_name.php | 4 +- .../20120111235330_duplicate_migration.php | 4 +- .../20120111235330_duplicate_migration_2.php | 4 +- .../20120111235330_invalid_class.php | 4 +- .../20120111235330_test_migration.php | 4 +- .../20120116183504_test_migration_2.php | 4 +- .../20180516025208_snapshot_pgsql.php | 4 +- .../20121213232502_create_initial_schema.php | 2 +- .../20121223011815_update_info_table.php | 2 +- ...49_rename_info_table_to_statuses_table.php | 2 +- ...20121224200739_rename_bio_to_biography.php | 2 +- ...0121224200852_create_user_logins_table.php | 2 +- ...24134305_direction_aware_reversible_up.php | 2 +- ...121929_direction_aware_reversible_down.php | 2 +- .../20180431121930_tricky_edge_case.php | 2 +- ...reate_test_index_limit_specifier_table.php | 2 +- .../20190928220334_add_column_index_fk.php | 2 +- ...207205056_should_not_execute_migration.php | 2 +- ...0201207205057_should_execute_migration.php | 2 +- 142 files changed, 1813 insertions(+), 1957 deletions(-) create mode 100644 rector.php diff --git a/composer.json b/composer.json index 2f2b54396..e4cea556e 100644 --- a/composer.json +++ b/composer.json @@ -76,6 +76,9 @@ "stan-setup": "phive install", "lowest": "validate-prefer-lowest", "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", + "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" && mv composer.backup composer.json", + "rector-check": "vendor/bin/rector process --dry-run", + "rector-fix": "vendor/bin/rector process", "test": "phpunit", "test-coverage": "phpunit --coverage-clover=clover.xml" } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2cda39ed2..9a7200326 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -60,12 +60,24 @@ parameters: count: 1 path: src/Migration/Environment.php + - + message: '#^Method Migrations\\Migration\\Manager\:\:getMigrationClassName\(\) should return class\-string\ but returns string\.$#' + identifier: return.type + count: 2 + path: src/Migration/Manager.php + - message: '#^Parameter \#1 \.\.\.\$arg1 of function max expects non\-empty\-array, array given\.$#' identifier: argument.type count: 1 path: src/Migration/Manager.php + - + message: '#^Parameter \#3 \$length of function substr expects int\|null, int\<0, max\>\|false given\.$#' + identifier: argument.type + count: 1 + path: src/Migration/Manager.php + - message: '#^Offset 0 on non\-empty\-list\ in isset\(\) always exists and is not nullable\.$#' identifier: isset.offset diff --git a/rector.php b/rector.php new file mode 100644 index 000000000..a93e7edcf --- /dev/null +++ b/rector.php @@ -0,0 +1,54 @@ +withPaths([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + + ->withCache( + cacheClass: FileCacheStorage::class, + cacheDirectory: $cacheDir, + ) + + ->withPhpSets() + ->withAttributesSets() + + ->withSets([ + SetList::CODE_QUALITY, + SetList::CODING_STYLE, + SetList::DEAD_CODE, + SetList::EARLY_RETURN, + SetList::INSTANCEOF, + SetList::TYPE_DECLARATION, + ]) + + ->withSkip([ + ClassPropertyAssignToConstructorPromotionRector::class, + CatchExceptionNameMatchingTypeRector::class, + ClosureToArrowFunctionRector::class, + RemoveUselessReturnTagRector::class, + CompactToVariablesRector::class, + ReturnTypeFromStrictFluentReturnRector::class, + SplitDoubleAssignRector::class, + NewlineAfterStatementRector::class, + ExplicitBoolCompareRector::class, + TypedPropertyFromCreateMockAssignRector::class, + ]); diff --git a/src/BaseMigration.php b/src/BaseMigration.php index 2969933aa..1e521d461 100644 --- a/src/BaseMigration.php +++ b/src/BaseMigration.php @@ -30,24 +30,18 @@ class BaseMigration implements MigrationInterface { /** * The Adapter instance - * - * @var \Migrations\Db\Adapter\AdapterInterface */ protected ?AdapterInterface $adapter = null; /** * The ConsoleIo instance - * - * @var \Cake\Console\ConsoleIo */ protected ?ConsoleIo $io = null; /** * The config instance. - * - * @var \Migrations\Config\ConfigInterface */ - protected ?ConfigInterface $config; + protected ?ConfigInterface $config = null; /** * List of all the table objects created by this migration @@ -58,15 +52,11 @@ class BaseMigration implements MigrationInterface /** * Is migrating up prop - * - * @var bool */ protected bool $isMigratingUp = true; /** * The version number. - * - * @var int */ protected int $version; @@ -77,8 +67,6 @@ class BaseMigration implements MigrationInterface * This option is global for all tables created in the migration file. * If you set it to false, you have to manually add the primary keys for your * tables using the Migrations\Table::addPrimaryKey() method - * - * @var bool */ public bool $autoId = true; @@ -110,7 +98,7 @@ public function setAdapter(AdapterInterface $adapter) */ public function getAdapter(): AdapterInterface { - if (!$this->adapter) { + if (!$this->adapter instanceof AdapterInterface) { throw new RuntimeException('Adapter not set.'); } @@ -458,18 +446,13 @@ public function index(string|array $columns): Index */ public function preFlightCheck(): void { - if (method_exists($this, MigrationInterface::CHANGE)) { - if ( - method_exists($this, MigrationInterface::UP) || - method_exists($this, MigrationInterface::DOWN) - ) { - $io = $this->getIo(); - if ($io) { - $io->out( - 'warning Migration contains both change() and up()/down() methods.' . - ' Ignoring up() and down().', - ); - } + if (method_exists($this, MigrationInterface::CHANGE) && (method_exists($this, MigrationInterface::UP) || method_exists($this, MigrationInterface::DOWN))) { + $io = $this->getIo(); + if ($io instanceof ConsoleIo) { + $io->out( + 'warning Migration contains both change() and up()/down() methods.' . + ' Ignoring up() and down().', + ); } } } diff --git a/src/BaseSeed.php b/src/BaseSeed.php index 146abc4e1..634fa9b27 100644 --- a/src/BaseSeed.php +++ b/src/BaseSeed.php @@ -25,31 +25,18 @@ class BaseSeed implements SeedInterface { /** * The Adapter instance - * - * @var \Migrations\Db\Adapter\AdapterInterface */ protected ?AdapterInterface $adapter = null; /** * The ConsoleIo instance - * - * @var \Cake\Console\ConsoleIo */ protected ?ConsoleIo $io = null; /** * The config instance. - * - * @var \Migrations\Config\ConfigInterface - */ - protected ?ConfigInterface $config; - - /** - * No-op constructor. */ - public function __construct() - { - } + protected ?ConfigInterface $config = null; /** * {@inheritDoc} @@ -81,7 +68,7 @@ public function setAdapter(AdapterInterface $adapter) */ public function getAdapter(): AdapterInterface { - if (!$this->adapter) { + if (!$this->adapter instanceof AdapterInterface) { throw new RuntimeException('Adapter not set.'); } @@ -130,10 +117,8 @@ public function setConfig(ConfigInterface $config) public function getName(): string { $name = static::class; - if (str_starts_with($name, 'Migrations\BaseSeed@anonymous')) { - if (preg_match('#[/\\\\]([a-zA-Z0-9_]+)\.php:#', $name, $matches)) { - $name = $matches[1]; - } + if (str_starts_with($name, 'Migrations\BaseSeed@anonymous') && preg_match('#[/\\\\](\w+)\.php:#', $name, $matches)) { + return $matches[1]; } return $name; @@ -238,7 +223,7 @@ public function isIdempotent(): bool public function call(string $seeder, array $options = []): void { $io = $this->getIo(); - if ($io === null) { + if (!$io instanceof ConsoleIo) { throw new RuntimeException('ConsoleIo is required for calling other seeders.'); } $io->out(''); @@ -287,7 +272,7 @@ protected function runCall(string $seeder, array $options = []): void 'source' => $options['source'], ]); $io = $this->getIo(); - if ($io === null) { + if (!$io instanceof ConsoleIo) { throw new RuntimeException('ConsoleIo is required for calling other seeders.'); } $manager = $factory->createManager($io); diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index c727b03ee..8b59fc715 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -29,9 +29,6 @@ */ class BakeMigrationCommand extends BakeSimpleMigrationCommand { - /** - * @var string - */ protected string $_name; /** @@ -45,7 +42,7 @@ public static function defaultName(): string /** * @inheritDoc */ - public function bake(string $name, Arguments $args, ConsoleIo $io): void + protected function bake(string $name, Arguments $args, ConsoleIo $io): void { EventManager::instance()->on('Bake.initialize', function (Event $event): void { /** @var \Bake\View\BakeView $view */ diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index 920af9371..08418fc0d 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -50,29 +50,21 @@ class BakeMigrationDiffCommand extends BakeSimpleMigrationCommand /** * Array of migrations that have already been migrated - * - * @var array */ protected array $migratedItems = []; /** * Path to the migration files - * - * @var string */ protected string $migrationsPath; /** * Migration files that are stored in the self::migrationsPath - * - * @var array */ protected array $migrationsFiles = []; /** * Name of the phinx log table - * - * @var string */ protected string $phinxTable; @@ -121,7 +113,7 @@ public static function defaultName(): string /** * @inheritDoc */ - public function bake(string $name, Arguments $args, ConsoleIo $io): void + protected function bake(string $name, Arguments $args, ConsoleIo $io): void { $this->setup($args); @@ -271,7 +263,7 @@ protected function getColumns(): void foreach ($addedColumns as $columnName) { $column = $this->safeGetColumn($currentSchema, $columnName); /** @var int $key */ - $key = array_search($columnName, $currentColumns); + $key = array_search($columnName, $currentColumns, true); if ($key > 0) { $column['after'] = $currentColumns[$key - 1]; } @@ -359,16 +351,14 @@ protected function getColumns(): void $this->templateData[$table]['columns']['remove'] = []; } $removedColumns = array_diff($oldColumns, $currentColumns); - if ($removedColumns) { - foreach ($removedColumns as $columnName) { - $column = $this->safeGetColumn($this->dumpSchema[$table], $columnName); - /** @var int $key */ - $key = array_search($columnName, $oldColumns); - if ($key > 0) { - $column['after'] = $oldColumns[$key - 1]; - } - $this->templateData[$table]['columns']['remove'][$columnName] = $column; + foreach ($removedColumns as $columnName) { + $column = $this->safeGetColumn($this->dumpSchema[$table], $columnName); + /** @var int $key */ + $key = array_search($columnName, $oldColumns, true); + if ($key > 0) { + $column['after'] = $oldColumns[$key - 1]; } + $this->templateData[$table]['columns']['remove'][$columnName] = $column; } } } @@ -475,10 +465,8 @@ protected function getIndexes(): void $removedIndexes = array_diff($oldIndexes, $currentIndexes); $parts = []; - if ($removedIndexes) { - foreach ($removedIndexes as $index) { - $parts[$index] = $this->dumpSchema[$table]->getIndex($index); - } + foreach ($removedIndexes as $index) { + $parts[$index] = $this->dumpSchema[$table]->getIndex($index); } $this->templateData[$table]['indexes']['remove'] = array_merge( $this->templateData[$table]['indexes']['remove'], @@ -516,7 +504,7 @@ protected function checkSync(): bool // Extract version numbers from current context's migration files $fileVersions = []; foreach ($this->migrationsFiles as $file) { - $filename = basename($file); + $filename = basename((string)$file); if (preg_match('/^(\d+)_/', $filename, $matches)) { $fileVersions[] = $matches[1]; } @@ -530,7 +518,7 @@ protected function checkSync(): bool } } - if (empty($contextMigratedVersions)) { + if ($contextMigratedVersions === []) { // No migrations from this context have been run yet return false; } @@ -539,7 +527,7 @@ protected function checkSync(): bool $lastVersion = max($contextMigratedVersions); $lastFile = end($this->migrationsFiles); - return $lastFile && str_contains($lastFile, $lastVersion); + return $lastFile && str_contains((string)$lastFile, $lastVersion); } /** @@ -555,6 +543,7 @@ protected function bakeSnapshot(string $name, Arguments $args, ConsoleIo $io): ? { $io->out('Your migrations history is empty and you do not have any migrations files.'); $io->out('Falling back to baking a snapshot...'); + $newArgs = []; $newArgs[] = $name; @@ -644,10 +633,13 @@ protected function getCurrentSchema(): array } foreach ($tablesToDescribe as $table) { - if (preg_match('/^.*phinxlog$/', $table) === 1) { + if (preg_match('/^.*phinxlog$/', (string)$table) === 1) { + continue; + } + if ($table === 'cake_migrations') { continue; } - if ($table === 'cake_migrations' || $table === 'cake_seeds') { + if ($table === 'cake_seeds') { continue; } diff --git a/src/Command/BakeMigrationSnapshotCommand.php b/src/Command/BakeMigrationSnapshotCommand.php index 259b62bfd..54afb2c85 100644 --- a/src/Command/BakeMigrationSnapshotCommand.php +++ b/src/Command/BakeMigrationSnapshotCommand.php @@ -35,9 +35,6 @@ class BakeMigrationSnapshotCommand extends BakeSimpleMigrationCommand use SnapshotTrait; use UtilTrait; - /** - * @var string - */ protected string $_name; /** @@ -51,7 +48,7 @@ public static function defaultName(): string /** * @inheritDoc */ - public function bake(string $name, Arguments $args, ConsoleIo $io): void + protected function bake(string $name, Arguments $args, ConsoleIo $io): void { $collection = $this->getCollection($this->connection); diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index 8cedabf45..cecf2ec99 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -32,20 +32,13 @@ class BakeSeedCommand extends SimpleBakeCommand { /** * path to Migration directory - * - * @var string */ public string $pathFragment = 'config/Seeds/'; - /** - * @var string - */ protected string $_name; /** * Arguments - * - * @var \Cake\Console\Arguments|null */ protected ?Arguments $args = null; @@ -160,7 +153,7 @@ public function templateData(Arguments $arguments): array /** * @inheritDoc */ - public function bake(string $name, Arguments $args, ConsoleIo $io): void + protected function bake(string $name, Arguments $args, ConsoleIo $io): void { $this->args = $args; /** @var array $options */ @@ -257,7 +250,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent if ($line[$j] === '\\') { // skip character right after an escape \ $j++; - } elseif ($line[$j] === '\'') { + } elseif ($line[$j] === "'") { // check string open/end $inString = !$inString; } @@ -272,7 +265,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent unset($line); // Remove marked lines - $lines = array_filter($lines, function ($line) { + $lines = array_filter($lines, function ($line): bool { return $line !== false; }); diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php index 622b61fcc..622f4d1cb 100644 --- a/src/Command/BakeSimpleMigrationCommand.php +++ b/src/Command/BakeSimpleMigrationCommand.php @@ -44,22 +44,16 @@ abstract class BakeSimpleMigrationCommand extends SimpleBakeCommand /** * path to Migration directory - * - * @var string */ public string $pathFragment = 'config'; /** * Console IO - * - * @var \Cake\Console\ConsoleIo|null */ protected ?ConsoleIo $io = null; /** * Arguments - * - * @var \Cake\Console\Arguments|null */ protected ?Arguments $args = null; @@ -154,7 +148,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int /** * @inheritDoc */ - public function bake(string $name, Arguments $args, ConsoleIo $io): void + protected function bake(string $name, Arguments $args, ConsoleIo $io): void { $this->io = $io; $this->args = $args; @@ -189,6 +183,7 @@ public function bake(string $name, Arguments $args, ConsoleIo $io): void $renderer = new TemplateRenderer($this->theme); $renderer->set('name', $name); $renderer->set($this->templateData($args)); + $contents = $renderer->generate($this->template()); $path = $this->getPath($args); @@ -271,7 +266,6 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar /** * If reserved PHP keyword. * - * @param string $name * @return bool */ protected function isReservedKeyword(string $name): bool diff --git a/src/Command/DumpCommand.php b/src/Command/DumpCommand.php index ff779e598..d0469a06b 100644 --- a/src/Command/DumpCommand.php +++ b/src/Command/DumpCommand.php @@ -45,7 +45,6 @@ public static function defaultName(): string /** * Extract options for the dump command from another migrations option parser. * - * @param \Cake\Console\Arguments $args * @return array */ public static function extractArgs(Arguments $args): array @@ -74,7 +73,7 @@ public static function extractArgs(Arguments $args): array * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'Dumps the current schema of the database to be used while baking a diff', @@ -133,21 +132,19 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $tables = $finder->getTablesToBake($collection, $options); $dump = []; - if ($tables) { - foreach ($tables as $table) { - $schema = $collection->describe($table); - $dump[$table] = $schema; - } + foreach ($tables as $table) { + $schema = $collection->describe($table); + $dump[$table] = $schema; } $filePath = $path . DS . 'schema-dump-' . $connectionName . '.lock'; - $io->verbose("Writing dump file `{$filePath}`..."); + $io->verbose(sprintf('Writing dump file `%s`...', $filePath)); if (file_put_contents($filePath, serialize($dump))) { - $io->verbose("Dump file `{$filePath}` was successfully written"); + $io->verbose(sprintf('Dump file `%s` was successfully written', $filePath)); return self::CODE_SUCCESS; } - $io->err("An error occurred while writing dump file `{$filePath}`"); + $io->err(sprintf('An error occurred while writing dump file `%s`', $filePath)); return self::CODE_ERROR; } diff --git a/src/Command/EntryCommand.php b/src/Command/EntryCommand.php index 3651d928d..0577b2dbc 100644 --- a/src/Command/EntryCommand.php +++ b/src/Command/EntryCommand.php @@ -31,8 +31,6 @@ class EntryCommand extends Command implements CommandCollectionAwareInterface { /** * The command collection to get help on. - * - * @var \Cake\Console\CommandCollection */ protected CommandCollection $commands; @@ -109,7 +107,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if ($args->hasArgumentAt(0)) { $name = $args->getArgumentAt(0); $io->err( - "Could not find migrations command named `$name`." + sprintf('Could not find migrations command named `%s`.', $name) . ' Run `migrations --help` to get a list of commands.', ); @@ -135,7 +133,7 @@ public function getHelp(): HelpCommand // Remove `migrations` array_shift($parts); - if (count($parts) === 0) { + if ($parts === []) { continue; } $commands[$command] = $class; diff --git a/src/Command/MarkMigratedCommand.php b/src/Command/MarkMigratedCommand.php index bb7cb6f44..6e86a36c3 100644 --- a/src/Command/MarkMigratedCommand.php +++ b/src/Command/MarkMigratedCommand.php @@ -42,7 +42,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'Mark a migration as applied', @@ -95,9 +95,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar */ protected function invalidOnlyOrExclude(Arguments $args): bool { - return ($args->getOption('exclude') && $args->getOption('only')) || - ($args->getOption('exclude') || $args->getOption('only')) && - $args->getOption('target') === null; + if ($args->getOption('exclude') && $args->getOption('only')) { + return true; + } + + return ($args->getOption('exclude') || $args->getOption('only')) && + $args->getOption('target') === null; } /** @@ -135,7 +138,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int } $output = $manager->markVersionsAsMigrated($path, $versions); - array_map(fn($line) => $io->out($line), $output); + array_map(fn(string $line): ?int => $io->out($line), $output); return self::CODE_SUCCESS; } diff --git a/src/Command/MigrateCommand.php b/src/Command/MigrateCommand.php index abc73655f..a5aab5f4f 100644 --- a/src/Command/MigrateCommand.php +++ b/src/Command/MigrateCommand.php @@ -50,7 +50,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'Apply migrations to a SQL datasource', @@ -152,7 +152,7 @@ protected function executeMigrations(Arguments $args, ConsoleIo $io): ?int if ($config->isDryRun()) { $io->info('DRY-RUN mode enabled'); } - $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using connection ' . $args->getOption('connection')); $io->verbose('using paths ' . $config->getMigrationPath()); $io->verbose('ordering by ' . $versionOrder . ' time'); diff --git a/src/Command/RollbackCommand.php b/src/Command/RollbackCommand.php index d3647f542..7e4ae14ec 100644 --- a/src/Command/RollbackCommand.php +++ b/src/Command/RollbackCommand.php @@ -51,7 +51,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'Rollback migrations to a specific migration', @@ -154,7 +154,7 @@ protected function executeMigrations(Arguments $args, ConsoleIo $io): ?int $config = $manager->getConfig(); $versionOrder = $config->getVersionOrder(); - $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using connection ' . $args->getOption('connection')); $io->verbose('using paths ' . $config->getMigrationPath()); $io->verbose('ordering by ' . $versionOrder . ' time'); diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php index 9ccb761b1..38154ea65 100644 --- a/src/Command/SeedCommand.php +++ b/src/Command/SeedCommand.php @@ -49,7 +49,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $description = [ 'Seed the database with data', @@ -166,7 +166,7 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int if ($fake) { $io->warning('performing fake seeding'); } - $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using connection ' . $args->getOption('connection')); $io->verbose('using paths ' . $config->getMigrationPath()); $io->verbose('ordering by ' . $versionOrder . ' time'); diff --git a/src/Command/SeedResetCommand.php b/src/Command/SeedResetCommand.php index f11964d03..f35daf45a 100644 --- a/src/Command/SeedResetCommand.php +++ b/src/Command/SeedResetCommand.php @@ -42,7 +42,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'The reset command removes seed execution records from the log', @@ -98,7 +98,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $io->info('DRY-RUN mode enabled'); } - $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using connection ' . $args->getOption('connection')); $io->verbose('using paths ' . $config->getSeedPath()); $seeds = $manager->getSeeds(); @@ -109,13 +109,13 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $seedsToReset = $seeds; if ($seedOption) { - $requestedSeeds = array_map('trim', explode(',', (string)$seedOption)); + $requestedSeeds = array_map(trim(...), explode(',', (string)$seedOption)); $seedsToReset = []; foreach ($requestedSeeds as $requestedSeed) { $normalizedName = $manager->normalizeSeedName($requestedSeed, $seeds); if ($normalizedName === null) { - $io->error("Seed `{$requestedSeed}` does not exist."); + $io->error(sprintf('Seed `%s` does not exist.', $requestedSeed)); return self::CODE_ERROR; } @@ -123,7 +123,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int } } - if (empty($seedsToReset)) { + if ($seedsToReset === []) { $io->warning('No seeds to reset.'); return self::CODE_SUCCESS; @@ -155,18 +155,18 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if (!$config->isDryRun()) { $adapter->removeSeedFromLog($seed); } - $io->info("Reset: {$seedName} seed"); + $io->info(sprintf('Reset: %s seed', $seedName)); $count++; } else { - $io->verbose("Skipped (not executed): {$seedName} seed"); + $io->verbose(sprintf('Skipped (not executed): %s seed', $seedName)); } } $io->out(''); if ($config->isDryRun()) { - $io->success("DRY-RUN: Would reset {$count} seed(s)."); + $io->success(sprintf('DRY-RUN: Would reset %d seed(s).', $count)); } else { - $io->success("Reset {$count} seed(s)."); + $io->success(sprintf('Reset %d seed(s).', $count)); } return self::CODE_SUCCESS; diff --git a/src/Command/SeedStatusCommand.php b/src/Command/SeedStatusCommand.php index 49a49d66f..68f983136 100644 --- a/src/Command/SeedStatusCommand.php +++ b/src/Command/SeedStatusCommand.php @@ -43,7 +43,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'The status command prints a list of all seeds, along with their execution status', @@ -91,7 +91,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $manager = $factory->createManager($io); $config = $manager->getConfig(); - $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using connection ' . $args->getOption('connection')); $io->verbose('using paths ' . $config->getSeedPath()); $seeds = $manager->getSeeds(); @@ -109,7 +109,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $appNamespace = Configure::read('App.namespace', 'App'); foreach ($seeds as $seed) { $plugin = null; - $className = get_class($seed); + $className = $seed::class; if (str_contains($className, '\\')) { $parts = explode('\\', $className); @@ -163,8 +163,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $io->out('Current seed execution status:'); $io->out(''); - $maxNameLength = max(array_map(fn($s) => strlen($s['seedName']), $statuses)); - $maxPluginLength = max(array_map(fn($s) => strlen($s['plugin'] ?? ''), $statuses)); + $maxNameLength = max(array_map(fn(array $s): int => strlen($s['seedName']), $statuses)); + $maxPluginLength = max(array_map(fn(array $s): int => strlen($s['plugin'] ?? ''), $statuses)); foreach ($statuses as $status) { $seedName = str_pad($status['seedName'], $maxNameLength); @@ -174,10 +174,10 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if ($status['status'] === 'executed') { $statusText = 'executed'; $date = $status['executedAt'] ? ' (' . $status['executedAt'] . ')' : ''; - $io->out(" {$statusText} {$plugin} {$seedName}{$date}{$idempotent}"); + $io->out(sprintf(' %s %s %s%s%s', $statusText, $plugin, $seedName, $date, $idempotent)); } else { $statusText = 'pending '; - $io->out(" {$statusText} {$plugin} {$seedName}{$idempotent}"); + $io->out(sprintf(' %s %s %s%s', $statusText, $plugin, $seedName, $idempotent)); } } diff --git a/src/Command/SeedsEntryCommand.php b/src/Command/SeedsEntryCommand.php index e353b7db5..cc0ea6f57 100644 --- a/src/Command/SeedsEntryCommand.php +++ b/src/Command/SeedsEntryCommand.php @@ -31,8 +31,6 @@ class SeedsEntryCommand extends Command implements CommandCollectionAwareInterfa { /** * The command collection to get help on. - * - * @var \Cake\Console\CommandCollection */ protected CommandCollection $commands; @@ -109,7 +107,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if ($args->hasArgumentAt(0)) { $name = $args->getArgumentAt(0); $io->err( - "Could not find seeds command named `$name`." + sprintf('Could not find seeds command named `%s`.', $name) . ' Run `seeds --help` to get a list of commands.', ); @@ -135,7 +133,7 @@ public function getHelp(): HelpCommand // Remove `seeds` array_shift($parts); - if (count($parts) === 0) { + if ($parts === []) { continue; } $commands[$command] = $class; diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php index 7d344c604..7fb7defcd 100644 --- a/src/Command/StatusCommand.php +++ b/src/Command/StatusCommand.php @@ -57,7 +57,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'The status command prints a list of all migrations, along with their current status', @@ -146,7 +146,6 @@ public function execute(Arguments $args, ConsoleIo $io): ?int /** * Print migration status to stdout. * - * @param array $migrations * @param \Cake\Console\ConsoleIo $io The console io * @param string $tableName The migration tracking table name * @return void diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index 140eb02f2..ba2ab65f3 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -51,7 +51,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'Upgrades migration tracking from legacy phinxlog tables to unified cake_migrations table.', @@ -114,20 +114,20 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $io->out(sprintf('Found %d phinxlog table(s):', count($legacyTables))); foreach ($legacyTables as $table => $plugin) { - $pluginLabel = $plugin === null ? '(app)' : "({$plugin})"; - $io->out(" - {$table} {$pluginLabel}"); + $pluginLabel = $plugin === null ? '(app)' : sprintf('(%s)', $plugin); + $io->out(sprintf(' - %s %s', $table, $pluginLabel)); } $io->out(''); // Create unified table if needed $unifiedTableName = UnifiedMigrationsTableStorage::TABLE_NAME; if (!$this->tableExists($connection, $unifiedTableName)) { - $io->out("Creating unified table {$unifiedTableName}..."); + $io->out(sprintf('Creating unified table %s...', $unifiedTableName)); if (!$dryRun) { $this->createUnifiedTable($connection, $io); } } else { - $io->out("Unified table {$unifiedTableName} already exists."); + $io->out(sprintf('Unified table %s already exists.', $unifiedTableName)); } $io->out(''); @@ -144,10 +144,10 @@ public function execute(Arguments $args, ConsoleIo $io): ?int if (!$dryRun) { // Clean up legacy tables $io->out(''); - foreach ($legacyTables as $tableName => $plugin) { + foreach (array_keys($legacyTables) as $tableName) { if ($dropTables) { - $io->out("Dropping legacy table {$tableName}..."); - $connection->execute("DROP TABLE {$connection->getDriver()->quoteIdentifier($tableName)}"); + $io->out(sprintf('Dropping legacy table %s...', $tableName)); + $connection->execute('DROP TABLE ' . $connection->getDriver()->quoteIdentifier($tableName)); } else { $io->out('Retaining legacy table. You should drop these tables once you have verified your upgrade.'); } @@ -158,12 +158,12 @@ public function execute(Arguments $args, ConsoleIo $io): ?int $io->out(''); $io->out('Next steps:'); if ($dropTables) { - $io->out(' 1. Set \'Migrations\' => [\'legacyTables\' => false] in your config'); + $io->out(" 1. Set 'Migrations' => ['legacyTables' => false] in your config"); $io->out(' 2. Test your application'); } else { $io->out(' 1. Test your application'); $io->out(' 2. Drop the phinxlog tables (re-run `bin/cake migrations upgrade --drop-tables`)'); - $io->out(' 3. Set \'Migrations\' => [\'legacyTables\' => false] in your config'); + $io->out(" 3. Set 'Migrations' => ['legacyTables' => false] in your config"); } } else { $io->out(''); @@ -299,7 +299,7 @@ protected function migrateTable( $rows = $query->execute()->fetchAll('assoc'); $count = count($rows); - $io->out("Migrating {$count} record(s) from {$tableName} ({$pluginLabel})..."); + $io->out(sprintf('Migrating %d record(s) from %s (%s)...', $count, $tableName, $pluginLabel)); if ($dryRun || $count === 0) { return $count; @@ -320,7 +320,7 @@ protected function migrateTable( 'breakpoint' => (int)($row['breakpoint'] ?? 0), ]); $insertQuery->execute(); - } catch (QueryException $e) { + } catch (QueryException) { $io->out('Already migrated ' . $row['migration_name'] . '.'); } } diff --git a/src/Config/Config.php b/src/Config/Config.php index 03361772d..829f1358b 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -28,9 +28,6 @@ class Config implements ConfigInterface */ public const VERSION_ORDER_EXECUTION_TIME = 'execution'; - /** - * @var array - */ protected array $values = []; /** @@ -114,7 +111,7 @@ public function isVersionOrderCreationTime(): bool { $versionOrder = $this->getVersionOrder(); - return $versionOrder == self::VERSION_ORDER_CREATION_TIME; + return $versionOrder === self::VERSION_ORDER_CREATION_TIME; } /** diff --git a/src/Config/ConfigInterface.php b/src/Config/ConfigInterface.php index 57e4ffc7f..e4cb37cc9 100644 --- a/src/Config/ConfigInterface.php +++ b/src/Config/ConfigInterface.php @@ -18,6 +18,7 @@ interface ConfigInterface extends ArrayAccess { public const DEFAULT_MIGRATION_FOLDER = 'Migrations'; + public const DEFAULT_SEED_FOLDER = 'Seeds'; /** diff --git a/src/Db/Action/Action.php b/src/Db/Action/Action.php index 4718b2682..73fddf752 100644 --- a/src/Db/Action/Action.php +++ b/src/Db/Action/Action.php @@ -12,9 +12,6 @@ abstract class Action { - /** - * @var \Migrations\Db\Table\TableMetadata - */ protected TableMetadata $table; /** diff --git a/src/Db/Action/AddColumn.php b/src/Db/Action/AddColumn.php index c4948740a..5029d727b 100644 --- a/src/Db/Action/AddColumn.php +++ b/src/Db/Action/AddColumn.php @@ -15,8 +15,6 @@ class AddColumn extends Action { /** * The column to add - * - * @var \Migrations\Db\Table\Column */ protected Column $column; diff --git a/src/Db/Action/AddForeignKey.php b/src/Db/Action/AddForeignKey.php index a1cfa00c5..f7a97cabd 100644 --- a/src/Db/Action/AddForeignKey.php +++ b/src/Db/Action/AddForeignKey.php @@ -15,8 +15,6 @@ class AddForeignKey extends Action { /** * The foreign key to add - * - * @var \Migrations\Db\Table\ForeignKey */ protected ForeignKey $foreignKey; diff --git a/src/Db/Action/AddIndex.php b/src/Db/Action/AddIndex.php index f818ed4c6..85d38a207 100644 --- a/src/Db/Action/AddIndex.php +++ b/src/Db/Action/AddIndex.php @@ -15,8 +15,6 @@ class AddIndex extends Action { /** * The index to add to the table - * - * @var \Migrations\Db\Table\Index */ protected Index $index; diff --git a/src/Db/Action/AddPartition.php b/src/Db/Action/AddPartition.php index aeb0a0bdc..a87d6feaf 100644 --- a/src/Db/Action/AddPartition.php +++ b/src/Db/Action/AddPartition.php @@ -16,9 +16,6 @@ */ class AddPartition extends Action { - /** - * @var \Migrations\Db\Table\PartitionDefinition - */ protected PartitionDefinition $partition; /** diff --git a/src/Db/Action/ChangeColumn.php b/src/Db/Action/ChangeColumn.php index 267e30aa9..cb2fa7192 100644 --- a/src/Db/Action/ChangeColumn.php +++ b/src/Db/Action/ChangeColumn.php @@ -15,15 +15,11 @@ class ChangeColumn extends Action { /** * The column definition - * - * @var \Migrations\Db\Table\Column */ protected Column $column; /** * The name of the column to be changed - * - * @var string */ protected string $columnName; @@ -41,7 +37,7 @@ public function __construct(TableMetadata $table, string $columnName, Column $co $this->column = $column; // if the name was omitted use the existing column name - if ($column->getName() === null || strlen((string)$column->getName()) === 0) { + if ($column->getName() === null || $column->getName() === '') { $column->setName($columnName); } } diff --git a/src/Db/Action/ChangeComment.php b/src/Db/Action/ChangeComment.php index 0fb773c90..d3b2d4f4b 100644 --- a/src/Db/Action/ChangeComment.php +++ b/src/Db/Action/ChangeComment.php @@ -14,8 +14,6 @@ class ChangeComment extends Action { /** * The new comment for the table - * - * @var string|null */ protected ?string $newComment = null; diff --git a/src/Db/Action/DropForeignKey.php b/src/Db/Action/DropForeignKey.php index c311f1bc8..5e9cfde9d 100644 --- a/src/Db/Action/DropForeignKey.php +++ b/src/Db/Action/DropForeignKey.php @@ -15,8 +15,6 @@ class DropForeignKey extends Action { /** * The foreign key to remove - * - * @var \Migrations\Db\Table\ForeignKey */ protected ForeignKey $foreignKey; diff --git a/src/Db/Action/DropIndex.php b/src/Db/Action/DropIndex.php index 4c9bbf014..9299dd36d 100644 --- a/src/Db/Action/DropIndex.php +++ b/src/Db/Action/DropIndex.php @@ -15,8 +15,6 @@ class DropIndex extends Action { /** * The index to drop - * - * @var \Migrations\Db\Table\Index */ protected Index $index; diff --git a/src/Db/Action/DropPartition.php b/src/Db/Action/DropPartition.php index 3647ff47d..e6b9c2cdb 100644 --- a/src/Db/Action/DropPartition.php +++ b/src/Db/Action/DropPartition.php @@ -15,9 +15,6 @@ */ class DropPartition extends Action { - /** - * @var string - */ protected string $partitionName; /** diff --git a/src/Db/Action/RemoveColumn.php b/src/Db/Action/RemoveColumn.php index 5aaa4a253..a73c5512e 100644 --- a/src/Db/Action/RemoveColumn.php +++ b/src/Db/Action/RemoveColumn.php @@ -15,8 +15,6 @@ class RemoveColumn extends Action { /** * The column to be removed - * - * @var \Migrations\Db\Table\Column */ protected Column $column; diff --git a/src/Db/Action/RenameColumn.php b/src/Db/Action/RenameColumn.php index 2565d753d..1092a5f7d 100644 --- a/src/Db/Action/RenameColumn.php +++ b/src/Db/Action/RenameColumn.php @@ -15,15 +15,11 @@ class RenameColumn extends Action { /** * The column to be renamed - * - * @var \Migrations\Db\Table\Column */ protected Column $column; /** * The new name for the column - * - * @var string */ protected string $newName; diff --git a/src/Db/Action/RenameTable.php b/src/Db/Action/RenameTable.php index 1f0b11e63..7b58ac653 100644 --- a/src/Db/Action/RenameTable.php +++ b/src/Db/Action/RenameTable.php @@ -14,8 +14,6 @@ class RenameTable extends Action { /** * The new name for the table - * - * @var string */ protected string $newName; diff --git a/src/Db/Action/SetPartitioning.php b/src/Db/Action/SetPartitioning.php index 0e24e048a..b56e32c6b 100644 --- a/src/Db/Action/SetPartitioning.php +++ b/src/Db/Action/SetPartitioning.php @@ -16,9 +16,6 @@ */ class SetPartitioning extends Action { - /** - * @var \Migrations\Db\Table\Partition - */ protected Partition $partition; /** diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 556a60787..2bf053717 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -64,9 +64,6 @@ abstract class AbstractAdapter implements AdapterInterface, DirectActionInterfac */ protected array $options = []; - /** - * @var \Cake\Console\ConsoleIo - */ protected ConsoleIo $io; /** @@ -74,24 +71,12 @@ abstract class AbstractAdapter implements AdapterInterface, DirectActionInterfac */ protected array $createdTables = []; - /** - * @var string - */ protected string $schemaTableName = 'phinxlog'; - /** - * @var string - */ protected string $seedSchemaTableName = 'cake_seeds'; - /** - * @var array - */ protected array $dataDomain = []; - /** - * @var \Cake\Database\Connection|null - */ protected ?Connection $connection = null; /** @@ -103,7 +88,7 @@ abstract class AbstractAdapter implements AdapterInterface, DirectActionInterfac public function __construct(array $options, ?ConsoleIo $io = null) { $this->setOptions($options); - if ($io !== null) { + if ($io instanceof ConsoleIo) { $this->setIo($io); } } @@ -197,7 +182,7 @@ protected function getSchemaDialect(): SchemaDialect */ public function getConnection(): Connection { - if ($this->connection === null) { + if (!$this->connection instanceof Connection) { $this->connection = $this->getOption('connection'); $this->connect(); } @@ -294,9 +279,9 @@ protected function verboseLog(string $message): void { $io = $this->getIo(); if ( - $io === null || ( + !$io instanceof ConsoleIo || ( !$this->isDryRunEnabled() && - $io->level() != ConsoleIo::VERBOSE + $io->level() !== ConsoleIo::VERBOSE ) ) { return; @@ -465,7 +450,7 @@ public function isDryRunEnabled(): bool protected function addCreatedTable(string $tableName): void { $tableName = $this->quoteTableName($tableName); - if (substr_compare($tableName, 'phinxlog', -strlen('phinxlog')) !== 0) { + if (!str_ends_with($tableName, 'phinxlog')) { $this->createdTables[] = $tableName; } } @@ -694,22 +679,18 @@ protected function generateInsertSql( $upsertClause = $this->getUpsertClause($mode, $updateColumns, $conflictColumns); if ($this->isDryRunEnabled()) { - $sql .= ' VALUES (' . implode(', ', array_map($this->quoteValue(...), $row)) . ')' . $upsertClause . ';'; - - return $sql; - } else { - $values = []; - foreach ($row as $value) { - $placeholder = '?'; - if ($value instanceof Literal) { - $placeholder = (string)$value; - } - $values[] = $placeholder; + return $sql . (' VALUES (' . implode(', ', array_map($this->quoteValue(...), $row)) . ')' . $upsertClause . ';'); + } + $values = []; + foreach ($row as $value) { + $placeholder = '?'; + if ($value instanceof Literal) { + $placeholder = (string)$value; } - $sql .= ' VALUES (' . implode(',', $values) . ')' . $upsertClause; - - return $sql; + $values[] = $placeholder; } + + return $sql . (' VALUES (' . implode(',', $values) . ')' . $upsertClause); } /** @@ -748,7 +729,7 @@ protected function getUpsertClause(?InsertMode $mode, ?array $updateColumns, ?ar if ($conflictColumns !== null && $conflictColumns !== []) { trigger_error( 'The $conflictColumns parameter is ignored by MySQL. ' . - 'MySQL\'s ON DUPLICATE KEY UPDATE applies to all unique constraints on the table.', + "MySQL's ON DUPLICATE KEY UPDATE applies to all unique constraints on the table.", E_USER_WARNING, ); } @@ -875,30 +856,27 @@ protected function generateBulkInsertSql( $upsertClause = $this->getUpsertClause($mode, $updateColumns, $conflictColumns); if ($this->isDryRunEnabled()) { - $values = array_map(function ($row) { + $values = array_map(function ($row): string { return '(' . implode(', ', array_map($this->quoteValue(...), $row)) . ')'; }, $rows); - $sql .= implode(', ', $values) . $upsertClause . ';'; - return $sql; - } else { - $queries = []; - foreach ($rows as $row) { - $values = []; - foreach ($row as $v) { - $placeholder = '?'; - if ($v instanceof Literal) { - $placeholder = (string)$v; - } - $values[] = $placeholder; + return $sql . (implode(', ', $values) . $upsertClause . ';'); + } + $queries = []; + foreach ($rows as $row) { + $values = []; + foreach ($row as $v) { + $placeholder = '?'; + if ($v instanceof Literal) { + $placeholder = (string)$v; } - $query = '(' . implode(', ', $values) . ')'; - $queries[] = $query; + $values[] = $placeholder; } - $sql .= implode(',', $queries) . $upsertClause; - - return $sql; + $query = '(' . implode(', ', $values) . ')'; + $queries[] = $query; } + + return $sql . implode(',', $queries) . $upsertClause; } /** @@ -969,7 +947,7 @@ protected function isUsingUnifiedTable(): bool // Autodetect mode (config is null or not set) // Check if the main legacy phinxlog table exists - if ($this->connection !== null) { + if ($this->connection instanceof Connection) { $dialect = $this->connection->getDriver()->schemaDialect(); if ($dialect->hasTable('phinxlog')) { return false; @@ -987,16 +965,11 @@ protected function isUsingUnifiedTable(): bool */ public function getVersionLog(): array { - switch ($this->options['version_order']) { - case Config::VERSION_ORDER_CREATION_TIME: - $orderBy = ['version' => 'ASC']; - break; - case Config::VERSION_ORDER_EXECUTION_TIME: - $orderBy = ['start_time' => 'ASC', 'version' => 'ASC']; - break; - default: - throw new RuntimeException('Invalid version_order configuration option'); - } + $orderBy = match ($this->options['version_order']) { + Config::VERSION_ORDER_CREATION_TIME => ['version' => 'ASC'], + Config::VERSION_ORDER_EXECUTION_TIME => ['start_time' => 'ASC', 'version' => 'ASC'], + default => throw new RuntimeException('Invalid version_order configuration option'), + }; $query = $this->migrationsTable()->getVersions($orderBy); // This will throw an exception if doing a --dry-run without any migrations as phinxlog @@ -1113,7 +1086,7 @@ public function getSeedLog(): array public function seedExecuted(SeedInterface $seed, string $executedTime): AdapterInterface { $plugin = null; - $className = get_class($seed); + $className = $seed::class; if (str_contains($className, '\\')) { $parts = explode('\\', $className); @@ -1144,7 +1117,7 @@ public function seedExecuted(SeedInterface $seed, string $executedTime): Adapter public function removeSeedFromLog(SeedInterface $seed): AdapterInterface { $plugin = null; - $className = get_class($seed); + $className = $seed::class; if (str_contains($className, '\\')) { $parts = explode('\\', $className); @@ -1278,7 +1251,7 @@ protected function getDefaultValueDefinition(mixed $default, ?string $columnType $default = $this->castToBool((bool)$default); } - return isset($default) ? " DEFAULT $default" : ''; + return isset($default) ? ' DEFAULT ' . $default : ''; } /** @@ -1291,7 +1264,7 @@ protected function getDefaultValueDefinition(mixed $default, ?string $columnType protected function executeAlterSteps(string $tableName, AlterInstructions $instructions): void { $alter = sprintf('ALTER TABLE %s %%s', $this->quoteTableName($tableName)); - $instructions->execute($alter, [$this, 'execute']); + $instructions->execute($alter, $this->execute(...)); } /** @@ -1710,7 +1683,6 @@ public function executeActions(TableMetadata $table, array $actions): void break; case $action instanceof DropTable: - /** @var \Migrations\Db\Action\DropTable $action */ $instructions->merge($this->getDropTableInstructions( $table->getName(), )); @@ -1777,7 +1749,7 @@ public function executeActions(TableMetadata $table, array $actions): void default: throw new InvalidArgumentException( - sprintf("Don't know how to execute action `%s`", get_class($action)), + sprintf("Don't know how to execute action `%s`", $action::class), ); } } diff --git a/src/Db/Adapter/AdapterFactory.php b/src/Db/Adapter/AdapterFactory.php index 4c7691f2b..b0dafb929 100644 --- a/src/Db/Adapter/AdapterFactory.php +++ b/src/Db/Adapter/AdapterFactory.php @@ -30,7 +30,7 @@ class AdapterFactory */ public static function instance(): static { - if (!static::$instance) { + if (!static::$instance instanceof AdapterFactory) { static::$instance = new static(); } @@ -72,7 +72,7 @@ public static function instance(): static public function registerAdapter(string $name, Closure|string $class) { if ( - !($class instanceof Closure || is_subclass_of($class, AdapterInterface::class)) + !$class instanceof Closure && !is_subclass_of($class, AdapterInterface::class) ) { throw new RuntimeException(sprintf( 'Adapter class `%s` must implement `Migrations\\Db\\Adapter\\AdapterInterface`', diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 0dad1ca37..bc3f2442e 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -29,29 +29,50 @@ interface AdapterInterface { public const TYPE_STRING = TableSchemaInterface::TYPE_STRING; + public const TYPE_CHAR = TableSchemaInterface::TYPE_CHAR; + public const TYPE_TEXT = TableSchemaInterface::TYPE_TEXT; + public const TYPE_INTEGER = TableSchemaInterface::TYPE_INTEGER; + public const TYPE_TINYINTEGER = TableSchemaInterface::TYPE_TINYINTEGER; + public const TYPE_SMALLINTEGER = TableSchemaInterface::TYPE_SMALLINTEGER; + public const TYPE_BIGINTEGER = TableSchemaInterface::TYPE_BIGINTEGER; + public const TYPE_FLOAT = TableSchemaInterface::TYPE_FLOAT; + public const TYPE_DECIMAL = TableSchemaInterface::TYPE_DECIMAL; + public const TYPE_DATETIME = TableSchemaInterface::TYPE_DATETIME; + public const TYPE_TIMESTAMP = TableSchemaInterface::TYPE_TIMESTAMP; + public const TYPE_TIME = TableSchemaInterface::TYPE_TIME; + public const TYPE_DATE = TableSchemaInterface::TYPE_DATE; + public const TYPE_BINARY = TableSchemaInterface::TYPE_BINARY; + public const TYPE_BINARY_UUID = TableSchemaInterface::TYPE_BINARY_UUID; + public const TYPE_BOOLEAN = TableSchemaInterface::TYPE_BOOLEAN; + public const TYPE_JSON = TableSchemaInterface::TYPE_JSON; + public const TYPE_UUID = TableSchemaInterface::TYPE_UUID; + public const TYPE_NATIVE_UUID = TableSchemaInterface::TYPE_NATIVE_UUID; // Geospatial database types public const TYPE_GEOMETRY = TableSchemaInterface::TYPE_GEOMETRY; + public const TYPE_POINT = TableSchemaInterface::TYPE_POINT; + public const TYPE_LINESTRING = TableSchemaInterface::TYPE_LINESTRING; + public const TYPE_POLYGON = TableSchemaInterface::TYPE_POLYGON; public const TYPES_GEOSPATIAL = [ @@ -63,90 +84,113 @@ interface AdapterInterface // only for mysql so far public const TYPE_YEAR = TableSchemaInterface::TYPE_YEAR; + public const TYPE_BIT = TableSchemaInterface::TYPE_BIT; // only for postgresql so far public const TYPE_CIDR = TableSchemaInterface::TYPE_CIDR; + public const TYPE_INET = TableSchemaInterface::TYPE_INET; + public const TYPE_MACADDR = TableSchemaInterface::TYPE_MACADDR; + public const TYPE_INTERVAL = TableSchemaInterface::TYPE_INTERVAL; /** * @deprecated 5.0.0 Use TYPE_STRING instead. */ public const PHINX_TYPE_STRING = self::TYPE_STRING; + /** * @deprecated 5.0.0 Use TYPE_CHAR instead. */ public const PHINX_TYPE_CHAR = self::TYPE_CHAR; + /** * @deprecated 5.0.0 Use TYPE_TEXT instead. */ public const PHINX_TYPE_TEXT = self::TYPE_TEXT; + /** * @deprecated 5.0.0 Use TYPE_INTEGER instead. */ public const PHINX_TYPE_INTEGER = self::TYPE_INTEGER; + /** * @deprecated 5.0.0 Use TYPE_TINYINTEGER instead. */ public const PHINX_TYPE_TINY_INTEGER = self::TYPE_TINYINTEGER; + /** * @deprecated 5.0.0 Use TYPE_SMALLINTEGER instead. */ public const PHINX_TYPE_SMALL_INTEGER = self::TYPE_SMALLINTEGER; + /** * @deprecated 5.0.0 Use TYPE_BIGINTEGER instead. */ public const PHINX_TYPE_BIG_INTEGER = self::TYPE_BIGINTEGER; + /** * @deprecated 5.0.0 Use TYPE_FLOAT instead. */ public const PHINX_TYPE_FLOAT = self::TYPE_FLOAT; + /** * @deprecated 5.0.0 Use TYPE_DECIMAL instead. */ public const PHINX_TYPE_DECIMAL = self::TYPE_DECIMAL; + /** * @deprecated 5.0.0 Use TYPE_DATETIME instead. */ public const PHINX_TYPE_DATETIME = self::TYPE_DATETIME; + /** * @deprecated 5.0.0 Use TYPE_TIMESTAMP instead. */ public const PHINX_TYPE_TIMESTAMP = self::TYPE_TIMESTAMP; + /** * @deprecated 5.0.0 Use TYPE_TIME instead. */ public const PHINX_TYPE_TIME = self::TYPE_TIME; + /** * @deprecated 5.0.0 Use TYPE_DATE instead. */ public const PHINX_TYPE_DATE = self::TYPE_DATE; + /** * @deprecated 5.0.0 Use TYPE_BINARY instead. */ public const PHINX_TYPE_BINARY = self::TYPE_BINARY; + /** * @deprecated 5.0.0 Use TYPE_BINARY_UUID instead. */ public const PHINX_TYPE_BINARYUUID = self::TYPE_BINARY_UUID; + /** * @deprecated 5.0.0 Use TYPE_BOOLEAN instead. */ public const PHINX_TYPE_BOOLEAN = self::TYPE_BOOLEAN; + /** * @deprecated 5.0.0 Use TYPE_JSON instead. */ public const PHINX_TYPE_JSON = self::TYPE_JSON; + /** * @deprecated 5.0.0 Use TableSchemaInterface::TYPE_JSON instead. */ public const PHINX_TYPE_JSONB = 'jsonb'; + /** * @deprecated 5.0.0 Use TYPE_UUID instead. */ public const PHINX_TYPE_UUID = self::TYPE_UUID; + /** * @deprecated 5.0.0 Use TYPE_NATIVE_UUID instead. */ @@ -156,14 +200,17 @@ interface AdapterInterface * @deprecated 5.0.0 Use TYPE_GEOMETRY instead. */ public const PHINX_TYPE_GEOMETRY = self::TYPE_GEOMETRY; + /** * @deprecated 5.0.0 Use TYPE_POINT instead. */ public const PHINX_TYPE_POINT = self::TYPE_POINT; + /** * @deprecated 5.0.0 Use TYPE_LINESTRING instead. */ public const PHINX_TYPE_LINESTRING = self::TYPE_LINESTRING; + /** * @deprecated 5.0.0 Use TYPE_POLYGON instead. */ @@ -188,14 +235,17 @@ interface AdapterInterface * @deprecated 5.0.0 Use TYPE_CIDR instead. */ public const PHINX_TYPE_CIDR = self::TYPE_CIDR; + /** * @deprecated 5.0.0 Use TYPE_INET instead. */ public const PHINX_TYPE_INET = self::TYPE_INET; + /** * @deprecated 5.0.0 Use TYPE_MACADDR instead. */ public const PHINX_TYPE_MACADDR = self::TYPE_MACADDR; + /** * @deprecated 5.0.0 Use TYPE_INTERVAL instead. */ diff --git a/src/Db/Adapter/AdapterWrapper.php b/src/Db/Adapter/AdapterWrapper.php index 597e9926d..e87b8473e 100644 --- a/src/Db/Adapter/AdapterWrapper.php +++ b/src/Db/Adapter/AdapterWrapper.php @@ -30,9 +30,6 @@ */ abstract class AdapterWrapper implements WrapperInterface { - /** - * @var \Migrations\Db\Adapter\AdapterInterface - */ protected AdapterInterface $adapter; /** diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index dabf3a95e..c8bb76e97 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -53,26 +53,32 @@ class MysqlAdapter extends AbstractAdapter * @deprecated 5.0.0 Enum column support will be removed in a future release. */ public const PHINX_TYPE_ENUM = 'enum'; + /** * @deprecated 5.0.0 Set column support will be removed in a future release. */ public const PHINX_TYPE_SET = 'set'; + /** * @deprecated 5.0.0 Use binary type with with no limit instead. */ public const PHINX_TYPE_BLOB = 'blob'; + /** * @deprecated 5.0.0 Use binary type with with limit BLOB_SMALL instead. */ public const PHINX_TYPE_TINYBLOB = 'tinyblob'; + /** * @deprecated 5.0.0 Use binary type with with limit BLOB_MEDIUM instead. */ public const PHINX_TYPE_MEDIUMBLOB = 'mediumblob'; + /** * @deprecated 5.0.0 Use binary type with with limit BLOB_LONG instead. */ public const PHINX_TYPE_LONGBLOB = 'longblob'; + /** * @deprecated 5.0.0 Use binary type instead. */ @@ -84,29 +90,45 @@ class MysqlAdapter extends AbstractAdapter // as its actual value is its regular value is larger than PHP_INT_MAX. We do this // to keep consistent the type hints for Column::$limit being integers. public const TEXT_TINY = 255; + public const TEXT_SMALL = 255; /* deprecated, alias of TEXT_TINY */ /** @deprecated Use length of null instead **/ public const TEXT_REGULAR = 65535; + public const TEXT_MEDIUM = 16777215; + public const TEXT_LONG = 2147483647; // According to https://dev.mysql.com/doc/refman/5.0/en/blob.html BLOB sizes are the same as TEXT public const BLOB_TINY = TableSchema::LENGTH_TINY; - public const BLOB_SMALL = TableSchema::LENGTH_TINY; /* deprecated, alias of BLOB_TINY */ + + public const BLOB_SMALL = TableSchema::LENGTH_TINY; + + /* deprecated, alias of BLOB_TINY */ public const BLOB_REGULAR = 65535; + public const BLOB_MEDIUM = TableSchema::LENGTH_MEDIUM; + public const BLOB_LONG = TableSchema::LENGTH_LONG; public const INT_TINY = 255; + public const INT_SMALL = 65535; + public const INT_MEDIUM = 16777215; + public const INT_REGULAR = 1073741823; + public const INT_BIG = 2147483647; public const INT_DISPLAY_TINY = 4; + public const INT_DISPLAY_SMALL = 6; + public const INT_DISPLAY_MEDIUM = 8; + public const INT_DISPLAY_REGULAR = 11; + public const INT_DISPLAY_BIG = 20; public const BIT = 64; @@ -154,8 +176,11 @@ class MysqlAdapter extends AbstractAdapter * @see https://mariadb.com/kb/en/alter-table/#algorithm */ public const ALGORITHM_DEFAULT = 'DEFAULT'; + public const ALGORITHM_INSTANT = 'INSTANT'; + public const ALGORITHM_INPLACE = 'INPLACE'; + public const ALGORITHM_COPY = 'COPY'; /** @@ -182,8 +207,11 @@ class MysqlAdapter extends AbstractAdapter * @see https://mariadb.com/kb/en/alter-table/#lock */ public const LOCK_DEFAULT = 'DEFAULT'; + public const LOCK_NONE = 'NONE'; + public const LOCK_SHARED = 'SHARED'; + public const LOCK_EXCLUSIVE = 'EXCLUSIVE'; /** @@ -218,7 +246,7 @@ public function hasTable(string $tableName): bool return true; } - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schema, $table] = explode('.', $tableName); $exists = $this->hasTableWithSchema($schema, $table); // Only break here on success, because it is possible for table names to contain a dot. @@ -306,7 +334,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in // process table collation if (isset($options['collation'])) { - $charset = explode('_', $options['collation']); + $charset = explode('_', (string)$options['collation']); $optionsStr .= sprintf(' CHARACTER SET %s', $charset[0]); $optionsStr .= sprintf(' COLLATE %s', $options['collation']); } @@ -354,7 +382,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in // add partitioning $partition = $table->getPartition(); - if ($partition !== null) { + if ($partition instanceof Partition) { $sql .= ' ' . $this->getPartitionSqlDefinition($partition); } @@ -450,7 +478,7 @@ protected function columnDefinitionSql(SchemaDialect $dialect, Column $column): $sql = $this->quoteColumnName($columnData['name']) . ' ' . $columnData['type']; $values = $column->getValues(); if ($values) { - $sql .= '(' . implode(', ', array_map(function ($value) { + $sql .= '(' . implode(', ', array_map(function ($value): string { // Special case NULL to trigger errors as it isn't allowed // in enum values. return $value === null ? 'NULL' : $this->quoteString($value); @@ -512,7 +540,7 @@ protected function getChangeCommentInstructions(TableMetadata $table, ?string $n $instructions = new AlterInstructions(); // passing 'null' is to remove table comment - $newComment = $newComment ?? ''; + $newComment ??= ''; $sql = sprintf(' COMMENT=%s ', $this->quoteString($newComment)); $instructions->addAlter($sql); @@ -632,7 +660,7 @@ public function getColumns(string $tableName): array $rawTypes = []; $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $row) { - $rawTypes[$row['Field']] = strtolower($row['Type']); + $rawTypes[$row['Field']] = strtolower((string)$row['Type']); } $columns = []; @@ -734,7 +762,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column $targetColumn = null; foreach ($columns as $column) { - if (strcasecmp($column->getName(), $columnName) === 0) { + if (strcasecmp((string)$column->getName(), $columnName) === 0) { $targetColumn = $column; break; } @@ -751,7 +779,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $row) { - if (strcasecmp($row['Field'], $columnName) === 0) { + if (strcasecmp((string)$row['Field'], $columnName) === 0) { $null = $row['Null'] === 'NO' ? 'NOT NULL' : 'NULL'; $comment = isset($row['Comment']) && $row['Comment'] !== '' ? ' COMMENT ' . $this->getConnection()->getDriver()->schemaValue($row['Comment']) @@ -759,8 +787,8 @@ protected function getRenameColumnInstructions(string $tableName, string $column // create the extra string by also filtering out the DEFAULT_GENERATED option (MySQL 8 fix) $extras = array_filter( - explode(' ', strtoupper($row['Extra'])), - static function ($value) { + explode(' ', strtoupper((string)$row['Extra'])), + static function (string $value): bool { return $value !== 'DEFAULT_GENERATED'; }, ); @@ -833,9 +861,8 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa protected function getIndexes(string $tableName): array { $dialect = $this->getSchemaDialect(); - $indexes = $dialect->describeIndexes($tableName); - return $indexes; + return $dialect->describeIndexes($tableName); } /** @@ -879,7 +906,7 @@ protected function getDropIndexByColumnsInstructions(string $tableName, $columns } $indexes = $this->getIndexes($tableName); - $columns = array_map('strtolower', $columns); + $columns = array_map(strtolower(...), $columns); foreach ($indexes as $index) { if ($columns == $index['columns']) { @@ -933,11 +960,10 @@ public function hasPrimaryKey(string $tableName, string|array $columns, ?string if ($constraint) { return $primaryKey['name'] === $constraint; - } else { - $missingColumns = array_diff((array)$columns, (array)$primaryKey['columns']); - - return empty($missingColumns); } + $missingColumns = array_diff((array)$columns, (array)$primaryKey['columns']); + + return $missingColumns === []; } /** @@ -972,9 +998,8 @@ public function getPrimaryKey(string $tableName): array protected function getForeignKeys(string $tableName): array { $dialect = $this->getSchemaDialect(); - $foreignKeys = $dialect->describeForeignKeys($tableName); - return $foreignKeys; + return $dialect->describeForeignKeys($tableName); } /** @@ -1012,12 +1037,12 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr { $instructions = new AlterInstructions(); - $columns = array_map('mb_strtolower', $columns); + $columns = array_map(mb_strtolower(...), $columns); $matches = []; $foreignKeys = $this->getForeignKeys($tableName); foreach ($foreignKeys as $key) { - if (array_map('mb_strtolower', $key['columns']) === $columns) { + if (array_map(mb_strtolower(...), $key['columns']) === $columns) { $matches[] = $key['name']; } } @@ -1165,7 +1190,7 @@ protected function getIndexSqlDefinition(Index $index): string $columnNames = (array)$index->getColumns(); $order = $index->getOrder() ?? []; - $columnNames = array_map(function ($columnName) use ($order) { + $columnNames = array_map(function (string $columnName) use ($order): string { $ret = $this->quoteColumnName($columnName); if (isset($order[$columnName])) { $ret .= ' ' . $order[$columnName]; @@ -1307,7 +1332,7 @@ protected function getPartitionSqlDefinition(Partition $partition): string if ($columns instanceof Literal) { $columnsSql = (string)$columns; } else { - $columnsSql = implode(', ', array_map(fn($col) => $this->quoteColumnName($col), $columns)); + $columnsSql = implode(', ', array_map($this->quoteColumnName(...), $columns)); } $sql = sprintf('PARTITION BY %s (%s)', $type, $columnsSql); @@ -1357,14 +1382,14 @@ protected function getPartitionDefinitionSql(string $type, PartitionDefinition $ if ($value === 'MAXVALUE' || $value === Partition::TYPE_RANGE . '_MAXVALUE') { $sql .= 'MAXVALUE'; } elseif (is_array($value)) { - $sql .= '(' . implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value)) . ')'; + $sql .= '(' . implode(', ', array_map($this->quotePartitionValue(...), $value)) . ')'; } else { $sql .= '(' . $this->quotePartitionValue($value) . ')'; } } elseif ($isListType) { $sql .= ' VALUES IN ('; if (is_array($value)) { - $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value)); + $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value)); } else { $sql .= $this->quotePartitionValue($value); } @@ -1425,7 +1450,7 @@ protected function getSetPartitioningInstructions(TableMetadata $table, Partitio */ protected function getAddPartitionsInstructions(TableMetadata $table, array $partitions): AlterInstructions { - if (empty($partitions)) { + if ($partitions === []) { return new AlterInstructions(); } @@ -1451,11 +1476,11 @@ protected function getAddPartitionsInstructions(TableMetadata $table, array $par */ protected function getDropPartitionsInstructions(string $tableName, array $partitionNames): AlterInstructions { - if (empty($partitionNames)) { + if ($partitionNames === []) { return new AlterInstructions(); } - $quotedNames = array_map(fn($name) => $this->quoteColumnName($name), $partitionNames); + $quotedNames = array_map($this->quoteColumnName(...), $partitionNames); $sql = 'DROP PARTITION ' . implode(', ', $quotedNames); return new AlterInstructions([$sql]); @@ -1486,7 +1511,7 @@ protected function getAddPartitionSql(PartitionDefinition $partition): string } elseif (is_array($value)) { // Likely LIST $sql .= ' VALUES IN ('; - $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value)); + $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value)); $sql .= ')'; } @@ -1506,7 +1531,7 @@ protected function getAddPartitionSql(PartitionDefinition $partition): string protected function hasNativeUuid(): bool { // Prevent infinite connect() loop when MysqlAdapter is used as a stub. - if ($this->connection === null || !$this->getOption('connection')) { + if (!$this->connection instanceof Connection || !$this->getOption('connection')) { return false; } $connection = $this->getConnection(); @@ -1523,7 +1548,7 @@ protected function hasNativeUuid(): bool protected function isMariaDb(): bool { // Prevent infinite connect() loop when MysqlAdapter is used as a stub. - if ($this->connection === null || !$this->getOption('connection')) { + if (!$this->connection instanceof Connection || !$this->getOption('connection')) { return false; } $connection = $this->getConnection(); diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index b0e61d220..162acc682 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -33,7 +33,9 @@ class PostgresAdapter extends AbstractAdapter protected const IDENTIFIER_MAX_LENGTH = 63; public const GENERATED_ALWAYS = 'ALWAYS'; + public const GENERATED_BY_DEFAULT = 'BY DEFAULT'; + /** * Allow insert when a column was created with the GENERATED ALWAYS clause. * This is required for seeding the database. @@ -65,8 +67,6 @@ class PostgresAdapter extends AbstractAdapter /** * Use identity columns if available (Postgres >= 10.0) - * - * @var bool */ protected bool $useIdentity; @@ -174,7 +174,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in if (is_string($options['primary_key'])) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($options['primary_key']); } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') - $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); + $sql .= implode(',', array_map($this->quoteColumnName(...), $options['primary_key'])); } $sql .= ')'; } else { @@ -185,24 +185,20 @@ public function createTable(TableMetadata $table, array $columns = [], array $in // add partitioning clause $partition = $table->getPartition(); - if ($partition !== null) { + if ($partition instanceof Partition) { $sql .= ' ' . $this->getPartitionSqlDefinition($partition); } $queries[] = $sql; // process column comments - if ($this->columnsWithComments) { - foreach ($this->columnsWithComments as $column) { - $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName()); - } + foreach ($this->columnsWithComments as $column) { + $queries[] = $this->getColumnCommentSqlDefinition($column, $table->getName()); } // set the indexes - if ($indexes) { - foreach ($indexes as $index) { - $queries[] = $this->getIndexSqlDefinition($index, $table->getName()); - } + foreach ($indexes as $index) { + $queries[] = $this->getIndexSqlDefinition($index, $table->getName()); } // process table comments @@ -215,7 +211,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in } // create partition tables for PostgreSQL declarative partitioning - if ($partition !== null) { + if ($partition instanceof Partition) { foreach ($partition->getDefinitions() as $definition) { $queries[] = $this->getPartitionTableSql($table->getName(), $partition, $definition); } @@ -280,7 +276,7 @@ protected function getChangePrimaryKeyInstructions(TableMetadata $table, array|s if (is_string($newColumns)) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($newColumns); } else { // handle primary_key => array('tag_id', 'resource_id') - $sql .= implode(',', array_map([$this, 'quoteColumnName'], $newColumns)); + $sql .= implode(',', array_map($this->quoteColumnName(...), $newColumns)); } $sql .= ')'; $instructions->addAlter($sql); @@ -426,7 +422,7 @@ protected function getRenameColumnInstructions( ]; $result = $this->query($sql, $params)->fetch('assoc'); if (!$result || !(bool)$result['column_exists']) { - throw new InvalidArgumentException("The specified column does not exist: $columnName"); + throw new InvalidArgumentException('The specified column does not exist: ' . $columnName); } $instructions = new AlterInstructions(); @@ -462,7 +458,7 @@ protected function getChangeColumnInstructions( // Remove the column name from $columnSql $columnType = preg_replace('/^"?(?:[^"]+)"?\s+/', '', $columnSql); // Remove generated clause - $columnType = preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', $columnType); + $columnType = preg_replace('/GENERATED (?:ALWAYS|BY DEFAULT) AS IDENTITY/', '', (string)$columnType); $sql = sprintf( 'ALTER COLUMN %s TYPE %s', @@ -475,13 +471,13 @@ protected function getChangeColumnInstructions( $quotedColumnName, ); } - if (in_array($newColumn->getType(), ['uuid', 'nativeuuid', 'binaryuuid'])) { + if (in_array($newColumn->getType(), ['uuid', 'nativeuuid', 'binaryuuid'], true)) { $sql .= sprintf( ' USING (%s::uuid)', $quotedColumnName, ); } - if (in_array($newColumn->getType(), ['json'])) { + if ($newColumn->getType() === 'json') { $sql .= sprintf( ' USING (%s::jsonb)', $quotedColumnName, @@ -489,9 +485,9 @@ protected function getChangeColumnInstructions( } // NULL and DEFAULT cannot be set while changing column type $sql = preg_replace('/ NOT NULL/', '', $sql); - $sql = preg_replace('/ DEFAULT NULL/', '', $sql); + $sql = preg_replace('/ DEFAULT NULL/', '', (string)$sql); // If it is set, DEFAULT is the last definition - $sql = preg_replace('/DEFAULT .*/', '', $sql); + $sql = preg_replace('/DEFAULT .*/', '', (string)$sql); if ($newColumn->getType() === 'boolean') { $sql .= sprintf( ' USING (CASE WHEN %s IS NULL THEN NULL WHEN %s::int=0 THEN FALSE ELSE TRUE END)', @@ -502,7 +498,7 @@ protected function getChangeColumnInstructions( $instructions->addAlter($sql); $column = $this->getColumn($tableName, $columnName); - assert($column !== null, 'Column must exist'); + assert($column instanceof Column, 'Column must exist'); if ($this->useIdentity) { // process identity @@ -512,9 +508,9 @@ protected function getChangeColumnInstructions( ); if ($newColumn->isIdentity() && $newColumn->getGenerated() !== null) { if ($column->isIdentity()) { - $sql .= sprintf(' SET GENERATED %s', (string)$newColumn->getGenerated()); + $sql .= sprintf(' SET GENERATED %s', $newColumn->getGenerated()); } else { - $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', (string)$newColumn->getGenerated()); + $sql .= sprintf(' ADD GENERATED %s AS IDENTITY', $newColumn->getGenerated()); } } else { $sql .= ' DROP IDENTITY IF EXISTS'; @@ -540,7 +536,7 @@ protected function getChangeColumnInstructions( $instructions->addAlter(sprintf( 'ALTER COLUMN %s SET %s', $quotedColumnName, - $this->getDefaultValueDefinition($newColumn->getDefault(), (string)$newColumn->getType()), + $this->getDefaultValueDefinition($newColumn->getDefault(), $newColumn->getType()), )); } elseif (!$newColumn->getIdentity()) { //drop default @@ -607,9 +603,8 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa protected function getIndexes(string $tableName): array { $dialect = $this->getSchemaDialect(); - $indexes = $dialect->describeIndexes($tableName); - return $indexes; + return $dialect->describeIndexes($tableName); } /** @@ -680,14 +675,13 @@ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint = if ($constraint) { return $primaryKey['constraint'] === $constraint; - } else { - if (is_string($columns)) { - $columns = [$columns]; // str to array - } - $missingColumns = array_diff($columns, $primaryKey['columns']); - - return empty($missingColumns); } + if (is_string($columns)) { + $columns = [$columns]; // str to array + } + $missingColumns = array_diff($columns, $primaryKey['columns']); + + return $missingColumns === []; } /** @@ -718,9 +712,8 @@ public function getPrimaryKey(string $tableName): array protected function getForeignKeys(string $tableName): array { $dialect = $this->getSchemaDialect(); - $foreignKeys = $dialect->describeForeignKeys($tableName); - return $foreignKeys; + return $dialect->describeForeignKeys($tableName); } /** @@ -789,9 +782,8 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr protected function getCheckConstraints(string $tableName): array { $dialect = $this->getSchemaDialect(); - $constraints = $dialect->describeCheckConstraints($tableName); - return $constraints; + return $dialect->describeCheckConstraints($tableName); } /** @@ -905,12 +897,12 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin $columnNames = (array)$index->getColumns(); $indexName = $index->getName(); - if ($indexName === null || strlen($indexName) === 0) { + if ($indexName === null || $indexName === '') { $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames)); } $order = $index->getOrder() ?? []; - $columnNames = array_map(function ($columnName) use ($order) { + $columnNames = array_map(function (string $columnName) use ($order): string { $ret = '"' . $columnName . '"'; if (isset($order[$columnName])) { $ret .= ' ' . $order[$columnName]; @@ -937,7 +929,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin $createIndexSentence, $index->getType() === Index::UNIQUE ? 'UNIQUE ' : '', $index->getConcurrently() ? ' CONCURRENTLY' : '', - $this->quoteColumnName((string)$indexName), + $this->quoteColumnName($indexName), $this->quoteTableName($tableName), implode(',', $columnNames), $includedColumns, @@ -961,13 +953,13 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta ' FOREIGN KEY (' . $columnList . ')' . ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . $refColumnList . ')'; if ($foreignKey->getOnDelete()) { - $def .= " ON DELETE {$foreignKey->getOnDelete()}"; + $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); } if ($foreignKey->getOnUpdate()) { - $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; + $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); } if ($foreignKey->getDeferrableMode()) { - $def .= " {$foreignKey->getDeferrableMode()}"; + $def .= ' ' . $foreignKey->getDeferrableMode(); } return $def; @@ -1138,7 +1130,11 @@ public function getColumnTypes(): array public function isValidColumnType(Column $column): bool { // If not a standard column type, maybe it is array type? - return parent::isValidColumnType($column) || $this->isArrayType($column->getType()); + if (parent::isValidColumnType($column)) { + return true; + } + + return $this->isArrayType($column->getType()); } /** @@ -1166,7 +1162,7 @@ protected function getSchemaName(string $tableName): array { $schema = $this->getGlobalSchemaName(); $table = $tableName; - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schema, $table] = explode('.', $tableName); } @@ -1291,7 +1287,7 @@ public function bulkinsert( $conflictClause = $this->getConflictClause($mode, $updateColumns, $conflictColumns); if ($this->isDryRunEnabled()) { - $values = array_map(function ($row) { + $values = array_map(function ($row): string { return '(' . implode(', ', array_map($this->quoteValue(...), $row)) . ')'; }, $rows); $sql .= implode(', ', $values) . $conflictClause . ';'; @@ -1388,7 +1384,7 @@ protected function getPartitionSqlDefinition(Partition $partition): string if ($columns instanceof Literal) { $columnsSql = (string)$columns; } else { - $columnsSql = implode(', ', array_map(fn($col) => $this->quoteColumnName($col), $columns)); + $columnsSql = implode(', ', array_map($this->quoteColumnName(...), $columns)); } return sprintf('PARTITION BY %s (%s)', $type, $columnsSql); @@ -1419,7 +1415,7 @@ protected function getPartitionTableSql(string $tableName, Partition $partition, } elseif ($type === Partition::TYPE_LIST) { $sql .= ' FOR VALUES IN ('; if (is_array($value)) { - $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value)); + $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value)); } else { $sql .= $this->quotePartitionValue($value); } @@ -1538,7 +1534,7 @@ private function getAddPartitionSql(TableMetadata $table, PartitionDefinition $p } elseif (is_array($value)) { // LIST partition $sql .= ' FOR VALUES IN ('; - $sql .= implode(', ', array_map(fn($v) => $this->quotePartitionValue($v), $value)); + $sql .= implode(', ', array_map($this->quotePartitionValue(...), $value)); $sql .= ')'; } else { // Simple RANGE (upper bound only) diff --git a/src/Db/Adapter/RecordingAdapter.php b/src/Db/Adapter/RecordingAdapter.php index aab4b41b3..cfcbddc34 100644 --- a/src/Db/Adapter/RecordingAdapter.php +++ b/src/Db/Adapter/RecordingAdapter.php @@ -107,7 +107,7 @@ public function getInvertedCommands(): Intent default: throw new IrreversibleMigrationException(sprintf( 'Cannot reverse a "%s" command', - get_class($command), + $command::class, )); } } diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 82a3812df..b32752ebd 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -59,9 +59,6 @@ class SqliteAdapter extends AbstractAdapter self::TYPE_UUID => 'uuid_text', ]; - /** - * @var string - */ protected string $suffix = '.sqlite3'; /** @@ -89,7 +86,7 @@ public function setOptions(array $options): AdapterInterface } //don't "fix" the file extension if it is blank, some people //might want a SQLITE db file with absolutely no extension. - if ($this->suffix !== '' && strpos($this->suffix, '.') !== 0) { + if ($this->suffix !== '' && !str_starts_with($this->suffix, '.')) { $this->suffix = '.' . $this->suffix; } @@ -129,11 +126,7 @@ protected function possiblyQuotedIdentifierRegex(string $identifier, bool $space $identifiers[] = "'" . ($hasSingleQuote ? str_replace("'", "''", $identifier) : $identifier) . "'"; if (!$hasTick && !$hasDoubleQuote && !$hasSingleQuote) { - if ($spacedNoQuotes) { - $identifiers[] = "\s+$identifier\s+"; - } else { - $identifiers[] = $identifier; - } + $identifiers[] = $spacedNoQuotes ? sprintf('\s+%s\s+', $identifier) : $identifier; } return '(' . implode('|', $identifiers) . ')'; @@ -179,7 +172,7 @@ protected function resolveTable(string $tableName): array // the temp schema is always first to be searched $schemata = ['temp']; foreach ($rows as $row) { - if (strtolower($row['name']) !== 'temp') { + if (strtolower((string)$row['name']) !== 'temp') { $schemata[] = $row['name']; } } @@ -190,9 +183,9 @@ protected function resolveTable(string $tableName): array $defaultSchema = $info['schema']; } - $table = strtolower($info['table']); + $table = strtolower((string)$info['table']); foreach ($schemata as $schema) { - if (strtolower($schema) === 'temp') { + if (strtolower((string)$schema) === 'temp') { $master = 'sqlite_temp_master'; } else { $master = sprintf('%s.%s', $this->quoteColumnName($schema), 'sqlite_master'); @@ -200,14 +193,14 @@ protected function resolveTable(string $tableName): array $rows = []; try { $result = $this->query( - "SELECT name FROM {$master} WHERE type = 'table' AND lower(name) = ?", + sprintf("SELECT name FROM %s WHERE type = 'table' AND lower(name) = ?", $master), [$table], ); // null on error if ($result !== null) { $rows = $result->fetchAll('assoc'); } - } catch (PDOException $e) { + } catch (PDOException) { // an exception can occur if the schema part of the table refers to a database which is not attached break; } @@ -215,7 +208,7 @@ protected function resolveTable(string $tableName): array // this somewhat pedantic check with strtolower is performed because the SQL lower function may be redefined, // and can act on all Unicode characters if the ICU extension is loaded, while SQL identifiers are only case-insensitive for ASCII foreach ($rows as $row) { - if (strtolower($row['name']) === $table) { + if (strtolower((string)$row['name']) === $table) { return ['schema' => $schema, 'table' => $row['name'], 'exists' => true]; } } @@ -292,7 +285,7 @@ public function createTable(TableMetadata $table, array $columns = [], array $in if (is_string($options['primary_key'])) { // handle primary_key => 'id' $sql .= $this->quoteColumnName($options['primary_key']); } elseif (is_array($options['primary_key'])) { // handle primary_key => array('tag_id', 'resource_id') - $sql .= implode(',', array_map([$this, 'quoteColumnName'], $options['primary_key'])); + $sql .= implode(',', array_map($this->quoteColumnName(...), $options['primary_key'])); } $sql .= ')'; } else { @@ -436,44 +429,50 @@ protected function parseDefaultValue(mixed $default, string $columnType): mixed PCRE_PATTERN; preg_match_all($pattern, $default, $matches); // strip out any comment tokens - $matches = array_map(function ($v) { + $matches = array_map(function (string $v): string { return preg_match('/^(?:\/\*|--)/', $v) ? ' ' : $v; }, $matches[0]); // reconstitute the string, trimming whitespace as well as parentheses $defaultClean = trim(implode('', $matches)); $defaultBare = rtrim(ltrim($defaultClean, $trimChars . '('), $trimChars . ')'); - // match the string against one of several patterns if ($columnType === TableSchemaInterface::TYPE_TEXT || $columnType === TableschemaInterface::TYPE_STRING) { // string literal return Literal::from($default); - } elseif ($columnType === TableSchemaInterface::TYPE_BOOLEAN) { + } + if ($columnType === TableSchemaInterface::TYPE_BOOLEAN) { // boolean literal return (int)filter_var($defaultClean, FILTER_VALIDATE_BOOLEAN); - } elseif (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $default)) { + } + if (preg_match('/^CURRENT_(?:DATE|TIME|TIMESTAMP)$/i', $default)) { // magic date or time return strtoupper($default); - } elseif (preg_match('/^[+-]?\d+$/i', $default)) { + } + if (preg_match('/^[+-]?\d+$/i', $default)) { $int = (int)$default; // integer literal if ($columnType === self::TYPE_BOOLEAN && ($int === 0 || $int === 1)) { return (bool)$int; - } else { - return $int; } - } elseif (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $default)) { + + return $int; + } + if (preg_match('/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i', $default)) { // float literal return (float)$default; - } elseif (preg_match('/^0x[0-9a-f]+$/i', $default)) { + } + if (preg_match('/^0x[0-9a-f]+$/i', $default)) { // hexadecimal literal return hexdec(substr($default, 2)); - } elseif (preg_match('/^null$/i', $defaultBare)) { + } + + // match the string against one of several patterns + if (preg_match('/^null$/i', $defaultBare)) { // null literal return null; - } else { - // any other expression: return the expression with parentheses, but without comments - return Expression::from($default); } + // any other expression: return the expression with parentheses, but without comments + return Expression::from($default); } /** @@ -489,24 +488,21 @@ protected function resolveIdentity(string $tableName): ?string $result = null; // make sure the table has only one primary key column which is of type integer foreach ($this->getColumnData($tableName) as $col) { - $type = strtolower($col['type']); + $type = strtolower((string)$col['type']); if ($col['pk'] > 1) { // the table has a composite primary key return null; - } elseif ($col['pk'] == 0) { + } + if ($col['pk'] == 0) { // the column is not a primary key column and is thus not relevant continue; - } elseif ($type !== 'integer') { + } + if ($type !== 'integer') { // if the primary key's type is not exactly INTEGER, it cannot be a row ID alias return null; - } else { - // the column is a candidate for a row ID alias - $result = $col['name']; } - } - // if there is no suitable PK column, stop now - if ($result === null) { - return null; + // the column is a candidate for a row ID alias + $result = $col['name']; } return $result; @@ -560,7 +556,7 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) use ($tableName, $column) { + $instructions->addPostStep(function (array $state) use ($tableName, $column): array { // we use the final column to anchor our regex to insert the new column, // as the alternative is unwinding all possible table constraints which // gets messy quickly with CHECK constraints. @@ -587,7 +583,7 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column return $state; }); - $instructions->addPostStep(function ($state) use ($tableName) { + $instructions->addPostStep(function ($state) use ($tableName): array { $newState = $this->calculateNewTableColumns($tableName, false, false); return $newState + $state; @@ -615,25 +611,23 @@ protected function getDeclaringSql(string $tableName): string $columnsInfo = $this->getColumnData($tableName); foreach ($columnsInfo as $column) { - $columnName = preg_quote($column['name'], '#'); - $columnNamePattern = "\"$columnName\"|`$columnName`|\\[$columnName\\]|$columnName"; - $columnNamePattern = "#([\(,]+\\s*)($columnNamePattern)(\\s)#iU"; + $columnName = preg_quote((string)$column['name'], '#'); + $columnNamePattern = sprintf('"%s"|`%s`|\[%s\]|%s', $columnName, $columnName, $columnName, $columnName); + $columnNamePattern = sprintf('#([\(,]+\s*)(%s)(\s)#iU', $columnNamePattern); $sql = preg_replace_callback( $columnNamePattern, - function ($matches) use ($column) { + function (array $matches) use ($column): string { return $matches[1] . $this->quoteColumnName($column['name']) . $matches[3]; }, - $sql, + (string)$sql, ); } - $tableNamePattern = "\"$tableName\"|`$tableName`|\\[$tableName\\]|$tableName"; - $tableNamePattern = "#^(CREATE TABLE)\s*($tableNamePattern)\s*(\()#Ui"; + $tableNamePattern = sprintf('"%s"|`%s`|\[%s\]|%s', $tableName, $tableName, $tableName, $tableName); + $tableNamePattern = sprintf('#^(CREATE TABLE)\s*(%s)\s*(\()#Ui', $tableNamePattern); - $sql = preg_replace($tableNamePattern, "$1 `$tableName` $3", $sql, 1); - - return $sql; + return preg_replace($tableNamePattern, sprintf('$1 `%s` $3', $tableName), (string)$sql, 1); } /** @@ -791,8 +785,8 @@ protected function updateIndicesForRenamedColumn( $state['indices'][$key]['sql'] = preg_replace( sprintf($pattern, preg_quote($oldColumnName, '/')), - "\\1\\2$newColumnName\\4\\5\\6", - $index['sql'], + sprintf('\1\2%s\4\5\6', $newColumnName), + (string)$index['sql'], ); } } @@ -852,14 +846,14 @@ protected function validateForeignKeys(AlterInstructions $instructions, string $ foreach ($otherTables as $otherTable) { $foreignKeyList = $this->getForeignKeys($otherTable['name']); foreach ($foreignKeyList as $foreignKey) { - if (strcasecmp($foreignKey['references'][0], $tableName) === 0) { + if (strcasecmp((string)$foreignKey['references'][0], $tableName) === 0) { $tablesToCheck[] = $otherTable['name']; break; } } } - $tablesToCheck = array_unique(array_map('strtolower', $tablesToCheck)); + $tablesToCheck = array_unique(array_map(strtolower(...), $tablesToCheck)); foreach ($tablesToCheck as $tableToCheck) { $schema = $this->getSchemaName($tableToCheck, true)['schema']; @@ -915,7 +909,7 @@ protected function copyDataToNewTable(string $tableName, string $tmpTableName, a */ protected function copyAndDropTmpTable(AlterInstructions $instructions, string $tableName): AlterInstructions { - $instructions->addPostStep(function ($state) use ($tableName) { + $instructions->addPostStep(function (array $state) use ($tableName): array { $this->copyDataToNewTable( $state['tmpTableName'], $tableName, @@ -974,14 +968,14 @@ protected function calculateNewTableColumns(string $tableName, string|false $col $writeColumns[] = $writeName; } } - $selectColumns = array_filter($selectColumns, fn($value) => $value !== ''); - $writeColumns = array_filter($writeColumns, fn($value) => $value !== ''); - $selectColumns = array_map([$this, 'quoteColumnName'], $selectColumns); - $writeColumns = array_map([$this, 'quoteColumnName'], $writeColumns); + $selectColumns = array_filter($selectColumns, fn($value): bool => $value !== ''); + $writeColumns = array_filter($writeColumns, fn($value): bool => $value !== ''); + $selectColumns = array_map($this->quoteColumnName(...), $selectColumns); + $writeColumns = array_map($this->quoteColumnName(...), $writeColumns); if ($columnName && !$found) { throw new InvalidArgumentException(sprintf( - 'The specified column doesn\'t exist: %s', + "The specified column doesn't exist: %s", $columnName, )); } @@ -999,8 +993,8 @@ protected function calculateNewTableColumns(string $tableName, string|false $col protected function beginAlterByCopyTable(string $tableName): AlterInstructions { $instructions = new AlterInstructions(); - $instructions->addPostStep(function ($state) use ($tableName) { - $tmpTableName = "tmp_{$tableName}"; + $instructions->addPostStep(function ($state) use ($tableName): array { + $tmpTableName = 'tmp_' . $tableName; $createSQL = $this->getDeclaringSql($tableName); // Table name in SQLite can be hilarious inside declaring SQL: @@ -1016,7 +1010,7 @@ protected function beginAlterByCopyTable(string $tableName): AlterInstructions $createSQL, ); - $createSQL = "CREATE TABLE {$this->quoteTableName($tmpTableName)} ({$createSQL}"; + $createSQL = sprintf('CREATE TABLE %s (%s', $this->quoteTableName($tmpTableName), $createSQL); return compact('createSQL', 'tmpTableName') + $state; }); @@ -1055,7 +1049,7 @@ protected function endAlterByCopyTable( } $result = $this->fetchRow('PRAGMA foreign_keys'); - $foreignKeysEnabled = $result ? (bool)$result['foreign_keys'] : false; + $foreignKeysEnabled = $result && (bool)$result['foreign_keys']; if ($foreignKeysEnabled) { $instructions->addPostStep('PRAGMA foreign_keys = OFF'); @@ -1072,7 +1066,7 @@ protected function endAlterByCopyTable( $foreignKeysEnabled && $validateForeignKeys ) { - $instructions = $this->validateForeignKeys($instructions, $tableName); + return $this->validateForeignKeys($instructions, $tableName); } return $instructions; @@ -1085,7 +1079,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column { $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) use ($columnName, $newColumnName) { + $instructions->addPostStep(function (array $state) use ($columnName, $newColumnName): array { $sql = str_replace( $this->quoteColumnName($columnName), $this->quoteColumnName($newColumnName), @@ -1096,7 +1090,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column return $state; }); - $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) { + $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName): array { $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName); return $newState + $state; @@ -1113,7 +1107,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column $instructions = $this->beginAlterByCopyTable($tableName); $newColumnName = (string)$newColumn->getName(); - $instructions->addPostStep(function ($state) use ($columnName, $newColumn) { + $instructions->addPostStep(function (array $state) use ($columnName, $newColumn): array { $dialect = $this->getSchemaDialect(); $sql = preg_replace( sprintf("/%s(?:\/\*.*?\*\/|\([^)]+\)|'[^']*?'|[^,])+([,)])/", $this->quoteColumnName($columnName)), @@ -1126,7 +1120,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column return $state; }); - $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName) { + $instructions->addPostStep(function ($state) use ($columnName, $newColumnName, $tableName): array { $newState = $this->calculateNewTableColumns($tableName, $columnName, $newColumnName); return $newState + $state; @@ -1142,20 +1136,20 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa { $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) use ($tableName, $columnName) { + $instructions->addPostStep(function ($state) use ($tableName, $columnName): array { $newState = $this->calculateNewTableColumns($tableName, $columnName, false); return $newState + $state; }); - $instructions->addPostStep(function ($state) use ($columnName) { + $instructions->addPostStep(function (array $state) use ($columnName): array { $sql = preg_replace( sprintf("/%s\s\w+.*(,\s(?!')|\)$)/U", preg_quote($this->quoteColumnName($columnName))), '', (string)$state['createSQL'], ); - if (substr($sql, -2) === ', ') { + if (str_ends_with($sql, ', ')) { $sql = substr($sql, 0, -2) . ')'; } @@ -1176,9 +1170,8 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa protected function getIndexes(string $tableName): array { $dialect = $this->getSchemaDialect(); - $indexes = $dialect->describeIndexes($tableName); - return $indexes; + return $dialect->describeIndexes($tableName); } /** @@ -1190,12 +1183,12 @@ protected function getIndexes(string $tableName): array */ protected function resolveIndex(string $tableName, string|array $columns): array { - $columns = array_map('strtolower', (array)$columns); + $columns = array_map(strtolower(...), (array)$columns); $indexes = $this->getIndexes($tableName); $matches = []; foreach ($indexes as $index) { - $indexCols = array_map('strtolower', $index['columns']); + $indexCols = array_map(strtolower(...), $index['columns']); if ($columns == $indexCols) { $matches[] = $index['name']; } @@ -1242,7 +1235,7 @@ protected function getDropIndexByColumnsInstructions(string $tableName, $columns if ($indexName === 'primary') { continue; } - if (strpos($indexName, 'sqlite_autoindex_') !== 0) { + if (!str_starts_with((string)$indexName, 'sqlite_autoindex_')) { $instructions->addPostStep(sprintf( 'DROP INDEX %s%s', $schema, @@ -1265,7 +1258,7 @@ protected function getDropIndexByNameInstructions(string $tableName, string $ind $found = false; foreach ($indexes as $index) { - if ($indexName === strtolower($index['name'])) { + if ($indexName === strtolower((string)$index['name'])) { $found = true; break; } @@ -1294,14 +1287,10 @@ public function hasPrimaryKey(string $tableName, $columns, ?string $constraint = throw new InvalidArgumentException('SQLite does not support named constraints.'); } - $columns = array_map('strtolower', (array)$columns); - $primaryKey = array_map('strtolower', $this->getPrimaryKey($tableName)); - - if (array_diff($primaryKey, $columns) || array_diff($columns, $primaryKey)) { - return false; - } + $columns = array_map(strtolower(...), (array)$columns); + $primaryKey = array_map(strtolower(...), $this->getPrimaryKey($tableName)); - return true; + return !array_diff($primaryKey, $columns) && !array_diff($columns, $primaryKey); } /** @@ -1332,9 +1321,8 @@ protected function getPrimaryKey(string $tableName): array protected function getForeignKeys(string $tableName): array { $dialect = $this->getSchemaDialect(); - $keys = $dialect->describeForeignKeys($tableName); - return $keys; + return $dialect->describeForeignKeys($tableName); } /** @@ -1347,26 +1335,22 @@ protected function getAddPrimaryKeyInstructions(TableMetadata $table, string $co $instructions = $this->beginAlterByCopyTable($table->getName()); $tableName = $table->getName(); - $instructions->addPostStep(function ($state) use ($column) { + $instructions->addPostStep(function (array $state) use ($column): array { $quotedColumn = preg_quote($column); - $columnPattern = "`{$quotedColumn}`|\"{$quotedColumn}\"|\[{$quotedColumn}\]"; - $matchPattern = "/($columnPattern)\s+(\w+(\(\d+\))?)(\s+(NOT )?NULL)?(\s+(?:PRIMARY KEY\s+)?AUTOINCREMENT)?/i"; + $columnPattern = sprintf('`%s`|"%s"|\[%s\]', $quotedColumn, $quotedColumn, $quotedColumn); + $matchPattern = sprintf('/(%s)\s+(\w+(\(\d+\))?)(\s+(NOT )?NULL)?(\s+(?:PRIMARY KEY\s+)?AUTOINCREMENT)?/i', $columnPattern); $sql = $state['createSQL']; - if (preg_match($matchPattern, $state['createSQL'], $matches)) { - if (isset($matches[2])) { - $hasAutoIncrement = isset($matches[6]) && stripos($matches[6], 'AUTOINCREMENT') !== false; - - if ($matches[2] === 'INTEGER' && $hasAutoIncrement) { - // Only add AUTOINCREMENT if the column already had it - $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT'; - } else { - $replace = '$1 $2 NOT NULL PRIMARY KEY'; - } - - $sql = preg_replace($matchPattern, $replace, (string)$state['createSQL'], 1); + if (preg_match($matchPattern, (string)$state['createSQL'], $matches) && isset($matches[2])) { + $hasAutoIncrement = isset($matches[6]) && stripos($matches[6], 'AUTOINCREMENT') !== false; + if ($matches[2] === 'INTEGER' && $hasAutoIncrement) { + // Only add AUTOINCREMENT if the column already had it + $replace = '$1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT'; + } else { + $replace = '$1 $2 NOT NULL PRIMARY KEY'; } + $sql = preg_replace($matchPattern, $replace, (string)$state['createSQL'], 1); } $this->execute($sql); @@ -1374,9 +1358,9 @@ protected function getAddPrimaryKeyInstructions(TableMetadata $table, string $co return $state; }); - $instructions->addPostStep(function ($state) { + $instructions->addPostStep(function (array $state): array { $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName']))); - $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name')); + $names = array_map($this->quoteColumnName(...), array_column($columns, 'name')); $selectColumns = $writeColumns = $names; return compact('selectColumns', 'writeColumns') + $state; @@ -1395,7 +1379,7 @@ protected function getDropPrimaryKeyInstructions(TableMetadata $table, string $c $tableName = $table->getName(); $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) { + $instructions->addPostStep(function (array $state): array { $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/"; $sql = preg_replace($search, '', (string)$state['createSQL'], 1); @@ -1406,7 +1390,7 @@ protected function getDropPrimaryKeyInstructions(TableMetadata $table, string $c return $state; }); - $instructions->addPostStep(function ($state) use ($column) { + $instructions->addPostStep(function (array $state) use ($column): array { $newState = $this->calculateNewTableColumns($state['tmpTableName'], $column, $column); return $newState + $state; @@ -1423,16 +1407,16 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey $instructions = $this->beginAlterByCopyTable($table->getName()); $tableName = $table->getName(); - $instructions->addPostStep(function ($state) use ($foreignKey, $tableName) { + $instructions->addPostStep(function (array $state) use ($foreignKey, $tableName): array { $this->execute('pragma foreign_keys = ON'); - $sql = substr($state['createSQL'], 0, -1) . ',' . $this->getForeignKeySqlDefinition($foreignKey, $tableName) . '); '; + $sql = substr((string)$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']; $tmpTableName = $state['tmpTableName']; $indexes = $this->getIndexes($tableName); foreach ($indexes as $index) { - if (strpos($index['name'], 'sqlite_autoindex_') !== 0) { + if (!str_starts_with((string)$index['name'], 'sqlite_autoindex_')) { $sql .= sprintf( 'DROP INDEX %s%s; ', $schema, @@ -1440,7 +1424,7 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey ); $createIndexSQL = $this->getDeclaringIndexSQL($tableName, $index['name']); $sql .= preg_replace( - "/\b{$tableName}\b/", + sprintf('/\b%s\b/', $tableName), $tmpTableName, $createIndexSQL, ); @@ -1452,9 +1436,9 @@ protected function getAddForeignKeyInstructions(TableMetadata $table, ForeignKey return $state; }); - $instructions->addPostStep(function ($state) { + $instructions->addPostStep(function (array $state): array { $columns = $this->fetchAll(sprintf('pragma table_info(%s)', $this->quoteTableName($state['tmpTableName']))); - $names = array_map([$this, 'quoteColumnName'], array_column($columns, 'name')); + $names = array_map($this->quoteColumnName(...), array_column($columns, 'name')); $selectColumns = $writeColumns = $names; return compact('selectColumns', 'writeColumns') + $state; @@ -1491,13 +1475,13 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) use ($columns) { + $instructions->addPostStep(function (array $state) use ($columns): array { $search = sprintf( "/,[^,]+?\(\s*%s\s*\)\s*REFERENCES[^,]*\([^\)]*\)[^,)]*/is", implode( '\s*,\s*', array_map( - fn($column) => $this->possiblyQuotedIdentifierRegex($column, false), + fn(string $column): string => $this->possiblyQuotedIdentifierRegex($column, false), $columns, ), ), @@ -1511,7 +1495,7 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr return $state; }); - $instructions->addPostStep(function ($state) { + $instructions->addPostStep(function (array $state): array { $newState = $this->calculateNewTableColumns($state['tmpTableName'], false, false); return $newState + $state; @@ -1541,7 +1525,7 @@ protected function getAddCheckConstraintInstructions(TableMetadata $table, Check $tableName = $table->getName(); $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) use ($checkConstraint) { + $instructions->addPostStep(function (array $state) use ($checkConstraint): array { $constraintName = $checkConstraint->getName(); if ($constraintName === null) { // Auto-generate constraint name if not provided @@ -1555,13 +1539,13 @@ protected function getAddCheckConstraintInstructions(TableMetadata $table, Check ); // Add the check constraint before the closing parenthesis - $sql = substr($state['createSQL'], 0, -1) . ', ' . $checkDef . ')'; + $sql = substr((string)$state['createSQL'], 0, -1) . ', ' . $checkDef . ')'; $this->execute($sql); return $state; }); - $instructions->addPostStep(function ($state) use ($tableName) { + $instructions->addPostStep(function ($state) use ($tableName): array { $newState = $this->calculateNewTableColumns($tableName, false, false); return $newState + $state; @@ -1577,11 +1561,11 @@ protected function getDropCheckConstraintInstructions(string $tableName, string { $instructions = $this->beginAlterByCopyTable($tableName); - $instructions->addPostStep(function ($state) use ($constraintName) { + $instructions->addPostStep(function (array $state) use ($constraintName): array { // Remove the check constraint from the CREATE TABLE statement // Match CONSTRAINT name CHECK (expression) or just CHECK (expression) $quotedName = $this->possiblyQuotedIdentifierRegex($constraintName, false); - $pattern = "/,?\s*CONSTRAINT\s+{$quotedName}\s+CHECK\s*\([^)]+(?:\([^)]*\)[^)]*)*\)/is"; + $pattern = sprintf('/,?\s*CONSTRAINT\s+%s\s+CHECK\s*\([^)]+(?:\([^)]*\)[^)]*)*\)/is', $quotedName); $sql = preg_replace($pattern, '', (string)$state['createSQL'], 1); if ($sql) { @@ -1591,7 +1575,7 @@ protected function getDropCheckConstraintInstructions(string $tableName, string return $state; }); - $instructions->addPostStep(function ($state) use ($tableName) { + $instructions->addPostStep(function ($state) use ($tableName): array { $newState = $this->calculateNewTableColumns($tableName, false, false); return $newState + $state; @@ -1640,11 +1624,7 @@ public function dropDatabase(string $name): void */ protected function getIndexSqlDefinition(TableMetadata $table, Index $index): string { - if ($index->getType() === Index::UNIQUE) { - $def = 'UNIQUE INDEX'; - } else { - $def = 'INDEX'; - } + $def = $index->getType() === Index::UNIQUE ? 'UNIQUE INDEX' : 'INDEX'; $indexName = $index->getName(); if ($indexName == '') { $indexName = $table->getName() . '_'; @@ -1653,9 +1633,8 @@ protected function getIndexSqlDefinition(TableMetadata $table, Index $index): st } $indexName .= 'index'; } - $def .= ' ' . $this->quoteColumnName($indexName); - return $def; + return $def . ' ' . $this->quoteColumnName($indexName); } /** diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index 67f08c00c..8867fdc4d 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -41,9 +41,6 @@ class SqlserverAdapter extends AbstractAdapter self::TYPE_NATIVE_UUID, ]; - /** - * @var string - */ protected string $schema = 'dbo'; /** @@ -221,7 +218,7 @@ protected function getColumnCommentSqlDefinition(Column $column, ?string $tableN // passing 'null' is to remove column comment $currentComment = $this->getColumnComment((string)$tableName, $column->getName()); - $comment = strcasecmp((string)$column->getComment(), 'NULL') !== 0 ? $this->quoteString((string)$column->getComment()) : '\'\''; + $comment = strcasecmp((string)$column->getComment(), 'NULL') !== 0 ? $this->quoteString((string)$column->getComment()) : "''"; $command = $currentComment === null ? 'sp_addextendedproperty' : 'sp_updateextendedproperty'; return sprintf( @@ -295,7 +292,7 @@ public function getColumnComment(string $tableName, ?string $columnName): ?strin $row = $this->query($sql, $params)->fetch('assoc'); if ($row) { - return trim($row['comment']); + return trim((string)$row['comment']); } return null; @@ -343,7 +340,7 @@ protected function parseDefault(?string $default): int|string|null $result = preg_replace(["/\('(.*)'\)/", "/\(\((.*)\)\)/", "/\((.*)\)/"], '$1', $default); - if (strtoupper($result) === 'NULL') { + if (strtoupper((string)$result) === 'NULL') { $result = null; } elseif (is_numeric($result)) { $result = (int)$result; @@ -375,13 +372,13 @@ protected function getAddColumnInstructions(TableMetadata $table, Column $column protected function getRenameColumnInstructions(string $tableName, string $columnName, string $newColumnName): AlterInstructions { if (!$this->hasColumn($tableName, $columnName)) { - throw new InvalidArgumentException("The specified column does not exist: $columnName"); + throw new InvalidArgumentException('The specified column does not exist: ' . $columnName); } $instructions = new AlterInstructions(); - $oldConstraintName = "DF_{$tableName}_{$columnName}"; - $newConstraintName = "DF_{$tableName}_{$newColumnName}"; + $oldConstraintName = sprintf('DF_%s_%s', $tableName, $columnName); + $newConstraintName = sprintf('DF_%s_%s', $tableName, $newColumnName); $sql = sprintf( 'IF (OBJECT_ID(%s, \'D\') IS NOT NULL) BEGIN @@ -394,7 +391,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column $instructions->addPostStep($sql); $instructions->addPostStep(sprintf( - 'EXECUTE sp_rename %s, %s, N\'COLUMN\'', + "EXECUTE sp_rename %s, %s, N'COLUMN'", $this->quoteString($tableName . '.' . $columnName), $this->quoteString($newColumnName), )); @@ -411,15 +408,11 @@ protected function getRenameColumnInstructions(string $tableName, string $column */ protected function getChangeDefault(string $tableName, Column $newColumn): AlterInstructions { - $constraintName = "DF_{$tableName}_{$newColumn->getName()}"; + $constraintName = sprintf('DF_%s_%s', $tableName, $newColumn->getName()); $default = $newColumn->getDefault(); $instructions = new AlterInstructions(); - if ($default === null) { - $default = 'DEFAULT NULL'; - } else { - $default = ltrim($this->getDefaultValueDefinition($default, (string)$newColumn->getType())); - } + $default = $default === null ? 'DEFAULT NULL' : ltrim($this->getDefaultValueDefinition($default, $newColumn->getType())); if (!$default) { return $instructions; @@ -450,7 +443,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column } } if ($oldColumn === null) { - throw new InvalidArgumentException("Unknown column {$columnName} cannot be changed."); + throw new InvalidArgumentException(sprintf('Unknown column %s cannot be changed.', $columnName)); } $changeDefault = @@ -480,6 +473,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column $dialect->columnDefinitionSql($columnData), ); $alterColumn = preg_replace('/DEFAULT NULL/', '', $alterColumn); + $instructions->addPostStep($alterColumn); // change column comment if needed @@ -562,7 +556,7 @@ protected function getIndexColumns(string $tableId, string $indexId): array $rows = $this->query($sql, $params)->fetchAll('assoc'); $columns = []; foreach ($rows as $row) { - $columns[] = strtolower($row['column_name']); + $columns[] = strtolower((string)$row['column_name']); } return $columns; @@ -603,7 +597,7 @@ protected function getDropIndexByColumnsInstructions(string $tableName, $columns } $indexes = $this->getIndexes($tableName); - $columns = array_map('strtolower', $columns); + $columns = array_map(strtolower(...), $columns); $instructions = new AlterInstructions(); foreach ($indexes as $index) { @@ -833,7 +827,7 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin $indexName = sprintf('%s_%s', $parts['table'], implode('_', $columnNames)); } $order = $index->getOrder() ?? []; - $columnNames = array_map(function ($columnName) use ($order) { + $columnNames = array_map(function (string $columnName) use ($order): string { $ret = '[' . $columnName . ']'; if (isset($order[$columnName])) { $ret .= ' ' . $order[$columnName]; @@ -877,10 +871,10 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta $def .= ' FOREIGN KEY (' . $columnList . ')'; $def .= ' REFERENCES ' . $this->quoteTableName($foreignKey->getReferencedTable()) . ' (' . $refColumnList . ')'; if ($foreignKey->getOnDelete()) { - $def .= " ON DELETE {$foreignKey->getOnDelete()}"; + $def .= ' ON DELETE ' . $foreignKey->getOnDelete(); } if ($foreignKey->getOnUpdate()) { - $def .= " ON UPDATE {$foreignKey->getOnUpdate()}"; + $def .= ' ON UPDATE ' . $foreignKey->getOnUpdate(); } return $def; @@ -1003,7 +997,7 @@ protected function getSchemaName(string $tableName): array { $schema = $this->getGlobalSchemaName(); $table = $tableName; - if (strpos($tableName, '.') !== false) { + if (str_contains($tableName, '.')) { [$schema, $table] = explode('.', $tableName); } @@ -1106,7 +1100,7 @@ public function bulkinsert( if ($v instanceof Literal) { $placeholder = (string)$v; } - if ($placeholder == '?') { + if ($placeholder === '?') { if ($v instanceof DateTime) { $vals[] = $v->toDateTimeString(); } elseif ($v instanceof Date) { diff --git a/src/Db/Adapter/TimedOutputAdapter.php b/src/Db/Adapter/TimedOutputAdapter.php index f5e11be62..4e7025d40 100644 --- a/src/Db/Adapter/TimedOutputAdapter.php +++ b/src/Db/Adapter/TimedOutputAdapter.php @@ -54,16 +54,16 @@ public function startCommandTimer(): callable public function writeCommand(string $command, array $args = []): void { $io = $this->getIo(); - if ($io && $io->level() < ConsoleIo::VERBOSE) { + if ($io instanceof ConsoleIo && $io->level() < ConsoleIo::VERBOSE) { return; } - if (count($args)) { + if ($args !== []) { $outArr = []; foreach ($args as $arg) { if (is_array($arg)) { $arg = array_map( - function ($value) { - return '\'' . $value . '\''; + function (string $value): string { + return "'" . $value . "'"; }, $arg, ); @@ -71,7 +71,7 @@ function ($value) { continue; } - $outArr[] = '\'' . $arg . '\''; + $outArr[] = "'" . $arg . "'"; } $this->getIo()?->verbose(' -- ' . $command . '(' . implode(', ', $outArr) . ')'); diff --git a/src/Db/Expression.php b/src/Db/Expression.php index 46d9a4f66..10b6dc5d2 100644 --- a/src/Db/Expression.php +++ b/src/Db/Expression.php @@ -8,7 +8,9 @@ namespace Migrations\Db; -class Expression +use Stringable; + +class Expression implements Stringable { /** * @var string The expression diff --git a/src/Db/Literal.php b/src/Db/Literal.php index b318daa14..843ad5645 100644 --- a/src/Db/Literal.php +++ b/src/Db/Literal.php @@ -8,11 +8,13 @@ namespace Migrations\Db; +use Stringable; + /** * Represent a value that should be used as a literal value when being * interpolated into SQL commands. */ -class Literal +class Literal implements Stringable { /** * @var string The literal's value diff --git a/src/Db/Plan/AlterTable.php b/src/Db/Plan/AlterTable.php index 47d0ac64a..3c5146139 100644 --- a/src/Db/Plan/AlterTable.php +++ b/src/Db/Plan/AlterTable.php @@ -18,8 +18,6 @@ class AlterTable { /** * The table - * - * @var \Migrations\Db\Table\TableMetadata */ protected TableMetadata $table; diff --git a/src/Db/Plan/NewTable.php b/src/Db/Plan/NewTable.php index 826283b6a..0e0723fc8 100644 --- a/src/Db/Plan/NewTable.php +++ b/src/Db/Plan/NewTable.php @@ -19,8 +19,6 @@ class NewTable { /** * The table to create - * - * @var \Migrations\Db\Table\TableMetadata */ protected TableMetadata $table; diff --git a/src/Db/Plan/Plan.php b/src/Db/Plan/Plan.php index dcbaa718d..b764a7b87 100644 --- a/src/Db/Plan/Plan.php +++ b/src/Db/Plan/Plan.php @@ -211,7 +211,7 @@ protected function resolveConflicts(): void $splitter = new ActionSplitter( RenameColumn::class, ChangeColumn::class, - function (RenameColumn $a, ChangeColumn $b) { + function (RenameColumn $a, ChangeColumn $b): bool { return $a->getNewName() === $b->getColumnName(); }, ); @@ -231,7 +231,7 @@ function (RenameColumn $a, ChangeColumn $b) { $splitter = new ActionSplitter( DropForeignKey::class, AddForeignKey::class, - function (DropForeignKey $a, AddForeignKey $b) { + function (DropForeignKey $a, AddForeignKey $b): bool { return $a->getForeignKey()->getColumns() === $b->getForeignKey()->getColumns(); }, ); @@ -308,7 +308,7 @@ protected function remapContraintAndIndexConflicts(AlterTable $alter): AlterTabl protected function forgetDropIndex(TableMetadata $table, array $columns, array $actions): array { $dropIndexActions = new ArrayObject(); - $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) { + $indexes = array_map(function (AlterTable $alter) use ($table, $columns, $dropIndexActions): AlterTable { if ($alter->getTable()->getName() !== $table->getName()) { return $alter; } @@ -340,7 +340,7 @@ protected function forgetDropIndex(TableMetadata $table, array $columns, array $ protected function forgetRemoveColumn(TableMetadata $table, array $columns, array $actions): array { $removeColumnActions = new ArrayObject(); - $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) { + $indexes = array_map(function (AlterTable $alter) use ($table, $columns, $removeColumnActions): AlterTable { if ($alter->getTable()->getName() !== $table->getName()) { return $alter; } @@ -407,8 +407,9 @@ protected function gatherUpdates(array $actions): void && !($action instanceof RemoveColumn) && !($action instanceof RenameColumn) ) { - continue; - } elseif (isset($this->tableCreates[$action->getTable()->getName()])) { + continue; + } + if (isset($this->tableCreates[$action->getTable()->getName()])) { continue; } $table = $action->getTable(); @@ -467,7 +468,8 @@ protected function gatherIndexes(array $actions): void foreach ($actions as $action) { if (!($action instanceof AddIndex) && !($action instanceof DropIndex)) { continue; - } elseif (isset($this->tableCreates[$action->getTable()->getName()])) { + } + if (isset($this->tableCreates[$action->getTable()->getName()])) { continue; } @@ -491,7 +493,7 @@ protected function gatherIndexes(array $actions): void protected function gatherConstraints(array $actions): void { foreach ($actions as $action) { - if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) { + if (!$action instanceof AddForeignKey && !$action instanceof DropForeignKey) { continue; } $table = $action->getTable(); @@ -520,7 +522,8 @@ protected function gatherPartitions(array $actions): void && !($action instanceof SetPartitioning) ) { continue; - } elseif (isset($this->tableCreates[$action->getTable()->getName()])) { + } + if (isset($this->tableCreates[$action->getTable()->getName()])) { continue; } diff --git a/src/Db/Plan/Solver/ActionSplitter.php b/src/Db/Plan/Solver/ActionSplitter.php index 48dd92a3e..358b30120 100644 --- a/src/Db/Plan/Solver/ActionSplitter.php +++ b/src/Db/Plan/Solver/ActionSplitter.php @@ -8,6 +8,7 @@ namespace Migrations\Db\Plan\Solver; +use Migrations\Db\Action\Action; use Migrations\Db\Plan\AlterTable; /** @@ -21,16 +22,12 @@ class ActionSplitter { /** * The fully qualified class name of the Action class to match for conflicts - * - * @var string */ protected string $conflictClass; /** * The fully qualified class name of the Action class to match for conflicts, which * is the dual of $conflictClass. For example `AddColumn` and `DropColumn` are duals. - * - * @var string */ protected string $conflictClassDual; @@ -70,7 +67,7 @@ public function __construct(string $conflictClass, string $conflictClassDual, ca */ public function __invoke(AlterTable $alter): array { - $conflictActions = array_filter($alter->getActions(), function ($action) { + $conflictActions = array_filter($alter->getActions(), function (Action $action): bool { return $action instanceof $this->conflictClass; }); diff --git a/src/Db/Table.php b/src/Db/Table.php index aeeb09066..151a3f677 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -51,30 +51,16 @@ */ class Table { - /** - * @var \Migrations\Db\Table\TableMetadata - */ protected TableMetadata $table; - /** - * @var \Migrations\Db\Adapter\AdapterInterface|null - */ protected ?AdapterInterface $adapter = null; - /** - * @var \Migrations\Db\Plan\Intent - */ protected Intent $actions; - /** - * @var array - */ protected array $data = []; /** * Insert mode for data operations - * - * @var \Migrations\Db\InsertMode|null */ protected ?InsertMode $insertMode = null; @@ -111,7 +97,7 @@ public function __construct(string $name, array $options = [], ?AdapterInterface $this->table = new TableMetadata($name, $options); $this->actions = new Intent(); - if ($adapter !== null) { + if ($adapter instanceof AdapterInterface) { $this->setAdapter($adapter); } } @@ -167,7 +153,7 @@ public function setAdapter(AdapterInterface $adapter) */ public function getAdapter(): AdapterInterface { - if (!$this->adapter) { + if (!$this->adapter instanceof AdapterInterface) { throw new RuntimeException('There is no database adapter set yet, cannot proceed'); } @@ -181,7 +167,11 @@ public function getAdapter(): AdapterInterface */ public function hasPendingActions(): bool { - return count($this->actions->getActions()) > 0 || count($this->data) > 0; + if ($this->actions->getActions() !== []) { + return true; + } + + return $this->data !== []; } /** @@ -277,7 +267,7 @@ public function getColumn(string $name): ?Column { $columns = array_filter( $this->getColumns(), - function ($column) use ($name) { + function (Column $column) use ($name): bool { return $column->getName() === $name; }, ); @@ -369,7 +359,7 @@ public function addColumn(string|Column $columnName, ?string $type = null, array if (!$this->getAdapter()->isValidColumnType($action->getColumn())) { throw new InvalidArgumentException(sprintf( 'An invalid column type "%s" was specified for column "%s".', - (string)$action->getColumn()->getType(), + $action->getColumn()->getType(), (string)$action->getColumn()->getName(), )); } @@ -459,13 +449,13 @@ public function changeColumn(string $columnName, string|Column|null $newColumnTy if ($newColumnType === null) { if (!$this->hasColumn($columnName)) { throw new RuntimeException( - "Cannot preserve column type for '$columnName' - column does not exist in table '{$this->getName()}'", + sprintf("Cannot preserve column type for '%s' - column does not exist in table '%s'", $columnName, $this->getName()), ); } $existingColumn = $this->getColumn($columnName); - if ($existingColumn === null) { + if (!$existingColumn instanceof Column) { throw new RuntimeException( - "Cannot retrieve column definition for '$columnName' in table '{$this->getName()}'", + sprintf("Cannot retrieve column definition for '%s' in table '%s'", $columnName, $this->getName()), ); } $newColumnType = $existingColumn->getType(); @@ -474,7 +464,7 @@ public function changeColumn(string $columnName, string|Column|null $newColumnTy if ($preserveUnspecified && $this->hasColumn($columnName)) { // Get existing column definition $existingColumn = $this->getColumn($columnName); - if ($existingColumn !== null) { + if ($existingColumn instanceof Column) { // Merge existing attributes with new ones $options = $this->mergeColumnOptions($existingColumn, $newColumnType, $options); } @@ -582,7 +572,7 @@ public function addForeignKey(string|array|ForeignKey $columns, string|TableMeta if ($columns instanceof ForeignKey) { $action = new AddForeignKey($this->table, $columns); } else { - if (!$referencedTable) { + if ($referencedTable === null) { throw new InvalidArgumentException('Referenced table is required'); } $action = AddForeignKey::build($this->table, $columns, $referencedTable, $referencedColumns, $options); @@ -646,7 +636,7 @@ public function partitionBy(string $type, string|array|Literal $columns, array $ public function addPartition(string $name, mixed $value = null, array $options = []) { $partition = $this->table->getPartition(); - if ($partition === null) { + if (!$partition instanceof Partition) { throw new RuntimeException('Must call partitionBy() before addPartition()'); } @@ -707,15 +697,15 @@ public function addPartitionToExisting(string $name, mixed $value, array $option */ public function addTimestamps(string|false|null $createdAt = 'created', string|false|null $updatedAt = 'updated', bool $withTimezone = false) { - $createdAt = $createdAt ?? 'created'; - $updatedAt = $updatedAt ?? 'updated'; + $createdAt ??= 'created'; + $updatedAt ??= 'updated'; if (!$createdAt && !$updatedAt) { throw new RuntimeException('Cannot set both created_at and updated_at columns to false'); } $timestampConfig = (bool)Configure::read('Migrations.add_timestamps_use_datetime'); $timestampType = 'timestamp'; - if ($timestampConfig === true) { + if ($timestampConfig) { $timestampType = 'datetime'; } @@ -778,7 +768,7 @@ public function insert(array $data) return $this; } - if (count($data) > 0) { + if ($data !== []) { $this->data[] = $data; } @@ -851,7 +841,7 @@ public function insertOrUpdate(array $data, array $updateColumns, array $conflic public function create(): void { $options = $this->getTable()->getOptions(); - if ((!isset($options['id']) || $options['id'] === false) && !empty($this->primaryKey)) { + if ((!isset($options['id']) || $options['id'] === false) && (isset($this->primaryKey) && !in_array($this->primaryKey, ['', '0', []], true))) { $options['primary_key'] = (array)$this->primaryKey; $this->filterPrimaryKey($options); } @@ -894,14 +884,14 @@ protected function filterPrimaryKey(array $options): void /** @var \Cake\Collection\Collection $columnsCollection */ $columnsCollection = (new Collection($this->actions->getActions())) - ->filter(function ($action) { + ->filter(function ($action): bool { return $action instanceof AddColumn; }) ->map(function ($action) { /** @var \Migrations\Db\Action\ChangeColumn|\Migrations\Db\Action\RenameColumn|\Migrations\Db\Action\RemoveColumn|\Migrations\Db\Action\AddColumn $action */ return $action->getColumn(); }); - $primaryKeyColumns = $columnsCollection->filter(function (Column $columnDef, $key) use ($primaryKey) { + $primaryKeyColumns = $columnsCollection->filter(function (Column $columnDef, $key) use ($primaryKey): bool { return isset($primaryKey[$columnDef->getName()]); })->toArray(); @@ -1036,7 +1026,7 @@ protected function executeActions(bool $exists): void // If table exists and has partition configuration, create SetPartitioning action if ($exists) { $partition = $this->table->getPartition(); - if ($partition !== null && $partition->getDefinitions()) { + if ($partition instanceof Partition && $partition->getDefinitions()) { $this->actions->addAction(new SetPartitioning($this->table, $partition)); } } @@ -1063,8 +1053,8 @@ protected function executeActions(bool $exists): void protected function mergeColumnOptions(Column $existingColumn, string $newColumnType, array $options): array { // Determine if type is changing - $newTypeString = (string)$newColumnType; - $existingTypeString = (string)$existingColumn->getType(); + $newTypeString = $newColumnType; + $existingTypeString = $existingColumn->getType(); $typeChanging = $newTypeString !== $existingTypeString; // Build array of existing column attributes diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index e608560ee..57a480471 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -42,84 +42,77 @@ class Column extends DatabaseColumn { public const BIGINTEGER = TableSchemaInterface::TYPE_BIGINTEGER; + public const SMALLINTEGER = TableSchemaInterface::TYPE_SMALLINTEGER; + public const TINYINTEGER = TableSchemaInterface::TYPE_TINYINTEGER; + public const BINARY = TableSchemaInterface::TYPE_BINARY; + public const BOOLEAN = TableSchemaInterface::TYPE_BOOLEAN; + public const CHAR = TableSchemaInterface::TYPE_CHAR; + public const DATE = TableSchemaInterface::TYPE_DATE; + public const DATETIME = TableSchemaInterface::TYPE_DATETIME; + public const DECIMAL = TableSchemaInterface::TYPE_DECIMAL; + public const FLOAT = TableSchemaInterface::TYPE_FLOAT; + public const INTEGER = TableSchemaInterface::TYPE_INTEGER; + public const STRING = TableSchemaInterface::TYPE_STRING; + public const TEXT = TableSchemaInterface::TYPE_TEXT; + public const TIME = TableSchemaInterface::TYPE_TIME; + public const TIMESTAMP = TableSchemaInterface::TYPE_TIMESTAMP; + public const UUID = TableSchemaInterface::TYPE_UUID; + public const BINARYUUID = TableSchemaInterface::TYPE_BINARY_UUID; + public const NATIVEUUID = TableSchemaInterface::TYPE_NATIVE_UUID; + /** MySQL-only column type */ public const YEAR = TableSchemaInterface::TYPE_YEAR; + /** MySQL/Postgres-only column type */ public const JSON = TableSchemaInterface::TYPE_JSON; + /** Postgres-only column type */ public const CIDR = TableSchemaInterface::TYPE_CIDR; + /** Postgres-only column type */ public const INET = TableSchemaInterface::TYPE_INET; + /** Postgres-only column type */ public const MACADDR = TableSchemaInterface::TYPE_MACADDR; + /** Postgres-only column type */ public const INTERVAL = TableSchemaInterface::TYPE_INTERVAL; - /** - * @var int|null - */ protected ?int $seed = null; - /** - * @var int|null - */ protected ?int $scale = null; - /** - * @var string|null - */ protected ?string $update = null; - /** - * @var bool - */ protected bool $timezone = false; - /** - * @var array - */ protected array $properties = []; - /** - * @var string|null - */ protected ?string $collation = null; - /** - * @var array|null - */ protected ?array $values = null; - /** - * @var string|null - */ protected ?string $algorithm = null; - /** - * @var string|null - */ protected ?string $lock = null; - /** - * @var bool|null - */ protected ?bool $fixed = null; /** @@ -142,7 +135,6 @@ class Column extends DatabaseColumn * @param int|null $srid The SRID for spatial columns. * @param string|null $encoding The character set encoding for the column. * @param string|null $baseType The base type for the column. - * @return void */ public function __construct( protected string $name = '', @@ -552,14 +544,9 @@ public function getUnsigned(): bool if ($this->identity && Configure::read('Migrations.unsigned_primary_keys')) { return true; } - // Check general integer configuration - if (Configure::read('Migrations.unsigned_ints')) { - return true; - } - // Default to signed for backward compatibility - return false; + return (bool)Configure::read('Migrations.unsigned_ints'); } /** diff --git a/src/Db/Table/ForeignKey.php b/src/Db/Table/ForeignKey.php index 3e4a4d394..f15e34472 100644 --- a/src/Db/Table/ForeignKey.php +++ b/src/Db/Table/ForeignKey.php @@ -23,11 +23,17 @@ class ForeignKey extends DatabaseForeignKey { public const CASCADE = 'CASCADE'; + public const RESTRICT = 'RESTRICT'; + public const SET_NULL = 'SET NULL'; + public const NO_ACTION = 'NO ACTION'; + public const DEFERRED = 'DEFERRABLE INITIALLY DEFERRED'; + public const IMMEDIATE = 'DEFERRABLE INITIALLY IMMEDIATE'; + public const NOT_DEFERRED = 'NOT DEFERRABLE'; /** diff --git a/src/Db/Table/Partition.php b/src/Db/Table/Partition.php index 12f8fe2a6..dffd7410a 100644 --- a/src/Db/Table/Partition.php +++ b/src/Db/Table/Partition.php @@ -18,10 +18,17 @@ class Partition { public const TYPE_RANGE = 'RANGE'; - public const TYPE_RANGE_COLUMNS = 'RANGE COLUMNS'; // MySQL: for DATE/STRING columns + + public const TYPE_RANGE_COLUMNS = 'RANGE COLUMNS'; + + // MySQL: for DATE/STRING columns public const TYPE_LIST = 'LIST'; - public const TYPE_LIST_COLUMNS = 'LIST COLUMNS'; // MySQL: for STRING columns + + public const TYPE_LIST_COLUMNS = 'LIST COLUMNS'; + + // MySQL: for STRING columns public const TYPE_HASH = 'HASH'; + public const TYPE_KEY = 'KEY'; // MySQL only /** diff --git a/src/Db/Table/TableMetadata.php b/src/Db/Table/TableMetadata.php index 09edd8500..3fc5b4a45 100644 --- a/src/Db/Table/TableMetadata.php +++ b/src/Db/Table/TableMetadata.php @@ -15,9 +15,6 @@ */ class TableMetadata { - /** - * @var string - */ protected string $name; /** @@ -25,9 +22,6 @@ class TableMetadata */ protected array $options; - /** - * @var \Migrations\Db\Table\Partition|null - */ protected ?Partition $partition = null; /** diff --git a/src/Middleware/PendingMigrationsMiddleware.php b/src/Middleware/PendingMigrationsMiddleware.php index 9c140a719..8c2ba5427 100644 --- a/src/Middleware/PendingMigrationsMiddleware.php +++ b/src/Middleware/PendingMigrationsMiddleware.php @@ -122,7 +122,6 @@ protected function checkAppMigrations(): bool } /** - * @param string $plugin * @return bool */ protected function checkPluginMigrations(string $plugin): bool @@ -158,7 +157,6 @@ protected function checkPluginMigrations(string $plugin): bool } /** - * @param \Psr\Http\Message\ServerRequestInterface $request * @return bool */ protected function isSkipped(ServerRequestInterface $request): bool diff --git a/src/Migration/BuiltinBackend.php b/src/Migration/BuiltinBackend.php index ab1118a31..9de414418 100644 --- a/src/Migration/BuiltinBackend.php +++ b/src/Migration/BuiltinBackend.php @@ -31,8 +31,6 @@ class BuiltinBackend implements BackendInterface { /** * Manager instance - * - * @var \Migrations\Migration\Manager|null */ protected ?Manager $manager = null; @@ -47,8 +45,6 @@ class BuiltinBackend implements BackendInterface * Current command being run. * Useful if some logic needs to be applied in the ConfigurationTrait depending * on the command - * - * @var string */ protected string $command; diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index b6861cae6..2dd7de09d 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -18,9 +18,6 @@ class Environment { - /** - * @var string - */ protected string $name; /** @@ -28,24 +25,12 @@ class Environment */ protected array $options; - /** - * @var \Cake\Console\ConsoleIo|null - */ protected ?ConsoleIo $io = null; - /** - * @var int - */ protected int $currentVersion; - /** - * @var string - */ protected string $schemaTableName = 'phinxlog'; - /** - * @var \Migrations\Db\Adapter\AdapterInterface - */ protected AdapterInterface $adapter; /** @@ -323,7 +308,7 @@ public function getAdapter(): AdapterInterface // Get the driver classname as those are aligned with adapter names. $driver = $connection->getDriver(); - $driverClass = get_class($driver); + $driverClass = $driver::class; $driverName = strtolower(substr($driverClass, (int)strrpos($driverClass, '\\') + 1)); $options['adapter'] = $driverName; @@ -340,7 +325,7 @@ public function getAdapter(): AdapterInterface } $io = $this->getIo(); - if ($io) { + if ($io instanceof ConsoleIo) { $adapter->setIo($io); } $this->setAdapter($adapter); diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 6baad31e8..8b77bfc4c 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -24,23 +24,16 @@ class Manager { public const BREAKPOINT_TOGGLE = 1; + public const BREAKPOINT_SET = 2; + public const BREAKPOINT_UNSET = 3; - /** - * @var \Migrations\Config\ConfigInterface - */ protected ConfigInterface $config; - /** - * @var \Cake\Console\ConsoleIo - */ protected ConsoleIo $io; - /** - * @var \Migrations\Migration\Environment|null - */ - protected ?Environment $environment; + protected ?Environment $environment = null; /** * @var \Migrations\MigrationInterface[]|null @@ -52,9 +45,6 @@ class Manager */ protected ?array $seeds = null; - /** - * @var \Psr\Container\ContainerInterface - */ protected ContainerInterface $container; /** @@ -225,7 +215,7 @@ public function isSeedExecuted(SeedInterface $seed): bool $seedLog = $adapter->getSeedLog(); $plugin = null; - $className = get_class($seed); + $className = $seed::class; if (str_contains($className, '\\')) { $parts = explode('\\', $className); @@ -306,7 +296,7 @@ public function markMigrated(int $version, string $path): bool } // Check if the file returns an anonymous class instance - if (is_object($migrationInstance) && $migrationInstance instanceof MigrationInterface) { + if ($migrationInstance instanceof MigrationInterface) { $migration = $migrationInstance; $migration->setVersion($version); } elseif (class_exists($className)) { @@ -336,15 +326,14 @@ public function markMigrated(int $version, string $path): bool */ protected function getMigrationClassName(string $path): string { - $class = (string)preg_replace('/^[0-9]+_/', '', basename($path)); + $class = (string)preg_replace('/^\d+_/', '', basename($path)); $class = str_replace('_', ' ', $class); $class = ucwords($class); $class = str_replace(' ', '', $class); - if (strpos($class, '.') !== false) { - $class = substr($class, 0, strpos($class, '.')); + if (str_contains($class, '.')) { + return substr($class, 0, strpos($class, '.')); } - /** @var class-string<\Migrations\MigrationInterface> */ return $class; } @@ -376,17 +365,17 @@ public function getVersionsToMark(Arguments $args): array if ($args->getOption('only') || $versionArg) { if (!in_array($version, $versions)) { - throw new InvalidArgumentException("Migration `$version` was not found !"); + throw new InvalidArgumentException(sprintf('Migration `%d` was not found !', $version)); } return [$version]; } $lengthIncrease = $args->getOption('exclude') ? 0 : 1; - $index = array_search($version, $versions); + $index = array_search($version, $versions, true); if ($index === false) { - throw new InvalidArgumentException("Migration `$version` was not found !"); + throw new InvalidArgumentException(sprintf('Migration `%d` was not found !', $version)); } return array_slice($versions, 0, $index + $lengthIncrease); @@ -460,15 +449,13 @@ public function migrate(?int $version = null, bool $fake = false, ?int $count = if ($version === null) { $version = max(array_merge($versions, array_keys($migrations))); - } else { - if ($version !== 0 && !isset($migrations[$version])) { - $this->getIo()->out(sprintf( - 'warning %s is not a valid version', - $version, - )); + } elseif ($version !== 0 && !isset($migrations[$version])) { + $this->getIo()->out(sprintf( + 'warning %s is not a valid version', + $version, + )); - return; - } + return; } // are we migrating up or down? @@ -583,14 +570,12 @@ public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake // Auto-execute missing dependencies $missingDeps = $this->getSeedDependenciesNotExecuted($seed); - if (!empty($missingDeps)) { - foreach ($missingDeps as $depSeed) { - $this->getIo()->verbose(sprintf( - ' Auto-executing dependency: %s', - $depSeed->getName(), - )); - $this->executeSeed($depSeed, $force, $fake); - } + foreach ($missingDeps as $depSeed) { + $this->getIo()->verbose(sprintf( + ' Auto-executing dependency: %s', + $depSeed->getName(), + )); + $this->executeSeed($depSeed, $force, $fake); } $this->printSeedStatus($seed, 'seeding'); @@ -733,7 +718,7 @@ public function rollback(int|string|null $target = null, bool $force = false, bo $target = 0; } elseif (!is_numeric($target) && $target !== null) { // try to find a target version based on name // search through the migrations using the name - $migrationNames = array_map(function ($item) { + $migrationNames = array_map(function (array $item) { return $item['migration_name']; }, $executedVersions); $found = array_search($target, $migrationNames, true); @@ -742,7 +727,7 @@ public function rollback(int|string|null $target = null, bool $force = false, bo if ($found !== false) { $target = (string)$found; } else { - $io->out("No migration found with name ($target)"); + $io->out(sprintf('No migration found with name (%s)', $target)); return; } @@ -765,7 +750,7 @@ public function rollback(int|string|null $target = null, bool $force = false, bo // If the target must match a version, check the target version exists if ($targetMustMatchVersion && $target !== 0 && !isset($migrations[$target])) { - $io->out("Target version ($target) not found"); + $io->out(sprintf('Target version (%s) not found', $target)); return; } @@ -781,13 +766,8 @@ public function rollback(int|string|null $target = null, bool $force = false, bo if (in_array($migration->getVersion(), $executedVersionCreationTimes)) { $executedArray = $executedVersions[$migration->getVersion()]; - if (!$targetMustMatchVersion) { - if ( - ($this->getConfig()->isVersionOrderCreationTime() && $executedArray['version'] <= $target) || - (!$this->getConfig()->isVersionOrderCreationTime() && $executedArray['start_time'] <= $target) - ) { - break; - } + if (!$targetMustMatchVersion && ($this->getConfig()->isVersionOrderCreationTime() && $executedArray['version'] <= $target || !$this->getConfig()->isVersionOrderCreationTime() && $executedArray['start_time'] <= $target)) { + break; } if ((int)$executedArray['breakpoint'] !== 0 && !$force) { @@ -843,7 +823,7 @@ public function seed(?string $seed = null, bool $force = false, bool $fake = fal */ public function getEnvironment(): Environment { - if (isset($this->environment)) { + if ($this->environment instanceof Environment) { return $this->environment; } @@ -885,7 +865,6 @@ public function getIo(): ConsoleIo /** * Replace the environment * - * @param \Migrations\Migration\Environment $environment * @return $this */ public function setEnvironment(Environment $environment) @@ -937,8 +916,8 @@ public function getMigrations(): array $io->verbose('Migration file'); $io->verbose( array_map( - function ($phpFile) { - return " {$phpFile}"; + function (string $phpFile): string { + return sprintf(' %s', $phpFile); }, $phpFiles, ), @@ -952,7 +931,7 @@ function ($phpFile) { $io = $this->getIo(); foreach ($phpFiles as $filePath) { if (Util::isValidMigrationFileName(basename($filePath))) { - $io->verbose("Valid migration file {$filePath}."); + $io->verbose(sprintf('Valid migration file %s.', $filePath)); $version = Util::getVersionFromFileName(basename($filePath)); @@ -973,7 +952,7 @@ function ($phpFile) { $fileNames[$class] = basename($filePath); - $io->verbose("Loading class $class from $filePath."); + $io->verbose(sprintf('Loading class %s from %s.', $class, $filePath)); $this->checkMigrationClass($filePath); @@ -992,13 +971,13 @@ function ($phpFile) { ini_set('display_errors', $orig_display_errors_setting); // Check if the file returns an anonymous class instance - if (is_object($migrationInstance) && $migrationInstance instanceof MigrationInterface) { - $io->verbose("Using anonymous class from $filePath."); + if ($migrationInstance instanceof MigrationInterface) { + $io->verbose(sprintf('Using anonymous class from %s.', $filePath)); $migration = $migrationInstance; $migration->setVersion($version); } elseif (class_exists($class)) { // Fall back to traditional class-based migration - $io->verbose("Constructing $class."); + $io->verbose(sprintf('Constructing %s.', $class)); $migration = new $class($version); } else { throw new InvalidArgumentException(sprintf( @@ -1015,7 +994,7 @@ function ($phpFile) { $versions[$version] = $migration; } else { - $io->verbose("Invalid migration file {$filePath}."); + $io->verbose(sprintf('Invalid migration file %s.', $filePath)); } } @@ -1205,19 +1184,15 @@ public function getSeeds(): array } // Check if the file returns an anonymous class instance - if (is_object($seedInstance) && $seedInstance instanceof SeedInterface) { - $io->verbose("Using anonymous class from $filePath."); + if ($seedInstance instanceof SeedInterface) { + $io->verbose(sprintf('Using anonymous class from %s.', $filePath)); $seed = $seedInstance; } elseif (class_exists($class)) { // Fall back to traditional class-based seed - $io->verbose("Instantiating $class."); + $io->verbose(sprintf('Instantiating %s.', $class)); // instantiate it /** @var \Migrations\SeedInterface $seed */ - if (isset($this->container)) { - $seed = $this->container->get($class); - } else { - $seed = new $class(); - } + $seed = isset($this->container) ? $this->container->get($class) : new $class(); } else { throw new InvalidArgumentException(sprintf( 'Could not find class `%s` in file `%s` and file did not return a seed instance', @@ -1432,7 +1407,7 @@ public function cleanupMissingMigrations(): int // Find missing migrations (those in migration table but not in filesystem) $missingVersions = []; - foreach ($versions as $versionId => $versionInfo) { + foreach (array_keys($versions) as $versionId) { if (!isset($defaultMigrations[$versionId])) { $missingVersions[] = $versionId; } diff --git a/src/Migration/ManagerFactory.php b/src/Migration/ManagerFactory.php index 362a06305..d4636f6c7 100644 --- a/src/Migration/ManagerFactory.php +++ b/src/Migration/ManagerFactory.php @@ -99,10 +99,10 @@ public function createConfig(): ConfigInterface $connectionConfig = ConnectionManager::getConfig($connectionName); } if (!$connectionConfig) { - throw new RuntimeException("Could not find connection `{$connectionName}`"); + throw new RuntimeException(sprintf('Could not find connection `%s`', $connectionName)); } if (!isset($connectionConfig['database'])) { - throw new RuntimeException("The `{$connectionName}` connection has no `database` key defined."); + throw new RuntimeException(sprintf('The `%s` connection has no `database` key defined.', $connectionName)); } $adapter = $connectionConfig['scheme'] ?? null; diff --git a/src/Migrations.php b/src/Migrations.php index b61bf07cf..3555f5f42 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -33,8 +33,6 @@ class Migrations * Current command being run. * Useful if some logic needs to be applied in the ConfigurationTrait depending * on the command - * - * @var string */ protected string $command = ''; diff --git a/src/TestSuite/Migrator.php b/src/TestSuite/Migrator.php index a0c30160b..c3c3991aa 100644 --- a/src/TestSuite/Migrator.php +++ b/src/TestSuite/Migrator.php @@ -23,9 +23,6 @@ class Migrator { - /** - * @var \Cake\TestSuite\ConnectionHelper - */ protected ConnectionHelper $helper; /** @@ -115,7 +112,7 @@ public function runMany( try { if (!$migrations->migrate($migrationSet)) { throw new RuntimeException( - "Unable to migrate fixtures for `{$migrationSet['connection']}`.", + sprintf('Unable to migrate fixtures for `%s`.', $migrationSet['connection']), ); } } catch (Exception $e) { @@ -124,7 +121,7 @@ public function runMany( "Migrations failed to apply with message:\n\n" . $e->getMessage() . "\n\n" . 'If you are using the `skip` option and running multiple sets of migrations ' . - 'on the same connection, you can\'t skip tables managed by CakePHP in the connection.', + "on the same connection, you can't skip tables managed by CakePHP in the connection.", 0, $e, ); @@ -170,7 +167,7 @@ public function truncate(string $connection, array $skip = []): void */ protected function shouldDropTables(Migrations $migrations, array $options): bool { - Log::write('debug', "Reading migrations status for {$options['connection']}..."); + Log::write('debug', sprintf('Reading migrations status for %s...', $options['connection'])); $messages = [ 'down' => [], @@ -179,24 +176,24 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo foreach ($migrations->status($options) as $migration) { if ($migration['status'] === 'up' && ($migration['missing'] ?? false)) { $messages['missing'][] = 'Applied but, missing Migration ' . - "source={$migration['name']} id={$migration['id']}"; + sprintf('source=%s id=%s', $migration['name'], $migration['id']); } if ($migration['status'] === 'down') { - $messages['down'][] = "Migration to reverse. source={$migration['name']} id={$migration['id']}"; + $messages['down'][] = sprintf('Migration to reverse. source=%s id=%s', $migration['name'], $migration['id']); } } $output = []; $hasProblems = false; - $itemize = function ($item) { + $itemize = function (string $item): string { return '- ' . $item; }; - if (!empty($messages['down'])) { + if ($messages['down'] !== []) { $hasProblems = true; $output[] = 'Migrations needing to be reversed:'; $output = array_merge($output, array_map($itemize, $messages['down'])); $output[] = ''; } - if (!empty($messages['missing'])) { + if ($messages['missing'] !== []) { $hasProblems = true; $output[] = 'Applied but missing migrations:'; $output = array_merge($output, array_map($itemize, $messages['missing'])); @@ -225,11 +222,11 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo protected function dropTables(string $connection, array $skip = []): void { $dropTables = $this->getNonPhinxTables($connection, $skip); - if (count($dropTables)) { + if ($dropTables !== []) { $this->helper->dropTables($connection, $dropTables); } $phinxTables = $this->getPhinxTables($connection); - if (count($phinxTables)) { + if ($phinxTables !== []) { $this->helper->truncateTables($connection, $phinxTables); } } @@ -246,8 +243,8 @@ protected function getPhinxTables(string $connection): array assert($connection instanceof Connection); $tables = $connection->getSchemaCollection()->listTables(); - return array_filter($tables, function ($table) { - return strpos($table, 'phinxlog') !== false; + return array_filter($tables, function (string $table): bool { + return str_contains($table, 'phinxlog'); }); } @@ -266,9 +263,9 @@ protected function getNonPhinxTables(string $connection, array $skip): array $skip[] = '*phinxlog*'; $skip[] = 'cake_migrations'; - return array_filter($tables, function ($table) use ($skip) { + return array_filter($tables, function (string $table) use ($skip): bool { foreach ($skip as $pattern) { - if (fnmatch($pattern, $table) === true) { + if (fnmatch($pattern, $table)) { return false; } } diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php index 97b9efdc0..c7ca1423b 100644 --- a/src/Util/ColumnParser.php +++ b/src/Util/ColumnParser.php @@ -16,8 +16,6 @@ class ColumnParser { /** * Regex used to parse the column definition passed through the shell - * - * @var string */ protected string $regexpParseColumn = '/ ^ @@ -36,8 +34,6 @@ class ColumnParser /** * Regex used to parse the field type and length - * - * @var string */ protected string $regexpParseField = '/(\w+\??)\[([0-9,]+)\]/'; @@ -74,7 +70,7 @@ public function parseFields(array $arguments): array $type = str_contains($type, '?') ? 'integer?' : 'integer'; } - $nullable = str_contains($type, '?'); + $nullable = str_contains((string)$type, '?'); $type = $nullable ? str_replace('?', '', $type) : $type; [$type, $length] = $this->getTypeAndLength($field, $type); @@ -94,7 +90,7 @@ public function parseFields(array $arguments): array } } - if ($isPrimaryKey === true && $type === 'integer') { + if ($isPrimaryKey && $type === 'integer') { $fields[$field]['options']['autoIncrement'] = true; } } @@ -120,15 +116,16 @@ public function parseIndexes(array $arguments): array $indexName = Hash::get($matches, 5); // Skip references - they create foreign keys, not indexes - if ($type && str_starts_with($type, 'references')) { + if ($type && str_starts_with((string)$type, 'references')) { continue; } - - if ( - in_array($type, ['primary', 'primary_key'], true) || - in_array($indexType, ['primary', 'primary_key'], true) || - $indexType === null - ) { + if (in_array($type, ['primary', 'primary_key'], true)) { + continue; + } + if (in_array($indexType, ['primary', 'primary_key'], true)) { + continue; + } + if ($indexType === null) { continue; } @@ -202,7 +199,7 @@ public function parseForeignKeys(array $arguments): array $indexName = Hash::get($matches, 5); // Check if type is 'references' or 'references?' - $isReference = str_starts_with($type, 'references'); + $isReference = str_starts_with((string)$type, 'references'); if (!$isReference) { continue; } @@ -242,7 +239,7 @@ public function validArguments(array $arguments): array { $collection = new Collection($arguments); - return $collection->filter(function ($value, $field) { + return $collection->filter(function ($value, $field): int|false { return preg_match($this->regexpParseColumn, (string)$field); })->toArray(); } @@ -259,11 +256,7 @@ public function getTypeAndLength(string $field, ?string $type): array { if ($type && preg_match($this->regexpParseField, $type, $matches)) { $length = $matches[2]; - if (str_contains($length, ',')) { - $length = array_map('intval', explode(',', $length)); - } else { - $length = (int)$length; - } + $length = str_contains($length, ',') ? array_map(intval(...), explode(',', $length)) : (int)$length; return [$matches[1], $length]; } @@ -287,9 +280,9 @@ public function getType(string $field, ?string $type): ?string $reflector = new ReflectionClass(AdapterInterface::class); $collection = new Collection($reflector->getConstants()); - $validTypes = $collection->filter(function ($value, $constant) { - return substr($constant, 0, strlen('TYPE_')) === 'TYPE_' || - substr($constant, 0, strlen('PHINX_TYPE_')) === 'PHINX_TYPE_'; + $validTypes = $collection->filter(function ($value, $constant): bool { + return str_starts_with($constant, 'TYPE_') || + str_starts_with($constant, 'PHINX_TYPE_'); })->toArray(); $fieldType = $type; if ($type === null || !in_array($type, $validTypes, true)) { @@ -297,7 +290,7 @@ public function getType(string $field, ?string $type): ?string $fieldType = 'integer'; } elseif ($field === 'id') { $fieldType = 'integer'; - } elseif (in_array($field, ['created', 'modified', 'updated'], true) || substr($field, -3) === '_at') { + } elseif (in_array($field, ['created', 'modified', 'updated'], true) || str_ends_with($field, '_at')) { $fieldType = 'datetime'; } elseif (in_array($field, ['latitude', 'longitude', 'lat', 'lng'], true)) { $fieldType = 'decimal'; @@ -402,12 +395,12 @@ public function parseDefaultValue(?string $value, string $columnType): string|in } // Handle integers - if (preg_match('/^-?[0-9]+$/', $value)) { + if (preg_match('/^-?\d+$/', $value)) { return (int)$value; } // Handle floats - if (preg_match('/^-?[0-9]+\.[0-9]+$/', $value)) { + if (preg_match('/^-?\d+\.\d+$/', $value)) { return (float)$value; } diff --git a/src/Util/TableFinder.php b/src/Util/TableFinder.php index a96b4e03b..5aeeb1f7f 100644 --- a/src/Util/TableFinder.php +++ b/src/Util/TableFinder.php @@ -34,8 +34,6 @@ class TableFinder /** * Regex of Table name to skip - * - * @var string */ public string $skipTablesRegex = '_phinxlog'; @@ -94,7 +92,7 @@ public function getTablesToBake(CollectionInterface $collection, array $options $tables = $tableNamesInPlugin; } else { foreach ($tables as $num => $table) { - if (in_array($table, $this->skipTables, true) || (strpos($table, $this->skipTablesRegex) !== false)) { + if (in_array($table, $this->skipTables, true) || (str_contains($table, $this->skipTablesRegex))) { unset($tables[$num]); continue; } @@ -138,17 +136,13 @@ public function getTableNames(?string $pluginName = null): array public function findTables(?string $pluginName = null): array { $path = 'Model' . DS . 'Table' . DS; - if ($pluginName) { - $path = CorePlugin::path($pluginName) . 'src' . DS . $path; - } else { - $path = APP . $path; - } + $path = $pluginName ? CorePlugin::path($pluginName) . 'src' . DS . $path : APP . $path; if (!is_dir($path)) { return []; } - return array_map('basename', glob($path . '*.php') ?: []); + return array_map(basename(...), glob($path . '*.php') ?: []); } /** diff --git a/src/Util/Util.php b/src/Util/Util.php index e8138cd09..25bf5e736 100644 --- a/src/Util/Util.php +++ b/src/Util/Util.php @@ -37,7 +37,7 @@ class Util * @var string * @phpstan-var non-empty-string */ - protected const MIGRATION_FILE_NAME_NO_NAME_PATTERN = '/^[0-9]{14}\.php$/'; + protected const MIGRATION_FILE_NAME_NO_NAME_PATTERN = '/^\d{14}\.php$/'; /** * Enhanced migration file name pattern with readable timestamp and CamelCase @@ -115,7 +115,7 @@ public static function getVersionFromFileName(string $fileName): int } // Traditional format - preg_match('/^[0-9]+/', $baseName, $matches); + preg_match('/^\d+/', $baseName, $matches); $value = (int)($matches[0] ?? null); if (!$value) { throw new RuntimeException(sprintf('Cannot get a valid version from filename `%s`', $fileName)); @@ -134,13 +134,12 @@ public static function getVersionFromFileName(string $fileName): int */ public static function mapClassNameToFileName(string $className): string { - $snake = function ($matches) { - return '_' . strtolower($matches[0]); + $snake = function ($matches): string { + return '_' . strtolower((string)$matches[0]); }; $fileName = preg_replace_callback('/\d+|[A-Z]/', $snake, $className); - $fileName = static::getCurrentTimestamp() . "$fileName.php"; - return $fileName; + return static::getCurrentTimestamp() . $fileName . '.php'; } /** @@ -250,7 +249,7 @@ public static function glob(string $path): array */ public static function getFiles(string|array $paths): array { - $files = static::globAll(array_map(function ($path) { + $files = static::globAll(array_map(function (string $path): string { return $path . DIRECTORY_SEPARATOR . '*.php'; }, (array)$paths)); // glob() can return the same file multiple times @@ -263,7 +262,6 @@ public static function getFiles(string|array $paths): array } /** - * @param string|null $plugin * @return string */ public static function tableName(?string $plugin): string diff --git a/src/Util/UtilTrait.php b/src/Util/UtilTrait.php index fc3ab3b89..ad49afee5 100644 --- a/src/Util/UtilTrait.php +++ b/src/Util/UtilTrait.php @@ -53,7 +53,7 @@ protected function getPhinxTable(?string $plugin = null, ?Connection $connection } // Autodetect mode (config is null or not set) - if ($connection !== null && $this->detectLegacyTables($connection)) { + if ($connection instanceof Connection && $this->detectLegacyTables($connection)) { return $this->getLegacyTableName($plugin); } @@ -113,7 +113,7 @@ protected function isUsingLegacyTables(?Connection $connection = null): bool } // Autodetect - if ($connection !== null) { + if ($connection instanceof Connection) { return $this->detectLegacyTables($connection); } diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 9aec4dae7..827fc8794 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -23,7 +23,6 @@ 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; @@ -76,22 +75,6 @@ public function getReturnedData(): array return $this->returnedData; } - /** - * Constructor - * - * ### Settings - * - * - `collection` \Cake\Database\Schema\Collection - * - `connection` \Cake\Database\Connection - * - * @param \Cake\View\View $View The View this helper is being attached to. - * @param array $config Configuration settings for the helper. - */ - public function __construct(View $View, array $config = []) - { - parent::__construct($View, $config); - } - /** * Returns the method to be used for the Table::save() * @@ -206,10 +189,8 @@ public function indexes(TableSchemaInterface|string $table): array $tableIndexes = $tableSchema->indexes(); $indexes = []; - if ($tableIndexes) { - foreach ($tableIndexes as $name) { - $indexes[$name] = $tableSchema->getIndex($name); - } + foreach ($tableIndexes as $name) { + $indexes[$name] = $tableSchema->getIndex($name); } return $indexes; @@ -466,7 +447,7 @@ public function getColumnOption(array $options): array */ public function value(string|float|int|bool|null $value, bool $numbersAsString = false): string|float { - if ($value === null || $value === 'null' || $value === 'NULL') { + if (in_array($value, [null, 'null', 'NULL'], true)) { return 'null'; } @@ -607,7 +588,7 @@ public function stringifyList(array $list, array $options = [], array $wantedOpt $v = $this->value($v, $k === 'default'); } if (!is_numeric($k)) { - $v = "'$k' => $v"; + $v = sprintf("'%s' => %s", $k, $v); } } @@ -632,14 +613,14 @@ public function stringifyList(array $list, array $options = [], array $wantedOpt */ public function tableStatement(string $table, bool $reset = false): string { - if ($reset === true) { + if ($reset) { unset($this->tableStatementStatus[$table]); } if (!isset($this->tableStatementStatus[$table])) { $this->tableStatementStatus[$table] = true; - return '$this->table(\'' . addslashes($table) . '\')'; + return '$this->table(\'' . addslashes($table) . "')"; } return ''; @@ -734,7 +715,7 @@ public function getCreateTablesElementData(array $tables): array $data = $this->getCreateTableData($table); $tableConstraintsNoUnique = array_filter( $data['constraints'], - function ($constraint) { + function (array $constraint): bool { return $constraint['type'] !== 'unique'; }, ); diff --git a/tests/TestCase/Command/BakeMigrationCommandTest.php b/tests/TestCase/Command/BakeMigrationCommandTest.php index 5e525e50a..aa1fd0f63 100644 --- a/tests/TestCase/Command/BakeMigrationCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationCommandTest.php @@ -33,14 +33,14 @@ class BakeMigrationCommandTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Migration' . DS; } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); $files = glob(ROOT . DS . 'config' . DS . 'Migrations' . DS . '*Users.php'); @@ -84,7 +84,7 @@ public function tearDown(): void * * @return void */ - public function testNoContents() + public function testNoContents(): void { $this->exec('bake migration NoContents --connection test'); @@ -101,7 +101,7 @@ public function testNoContents() * * @return array */ - public static function nameVariations() + public static function nameVariations(): array { return [ ['name', '.php'], @@ -118,9 +118,9 @@ public static function nameVariations() * @return void */ #[DataProvider('nameVariations')] - public function testCreate($name, $fileSuffix) + public function testCreate(string $name, string $fileSuffix): void { - $this->exec("bake migration CreateUsers {$name} --connection test"); + $this->exec(sprintf('bake migration CreateUsers %s --connection test', $name)); $file = glob(ROOT . DS . 'config' . DS . 'Migrations' . DS . '*_CreateUsers.php'); $filePath = current($file); @@ -133,7 +133,7 @@ public function testCreate($name, $fileSuffix) /** * Tests that baking a migration with the name as another will throw an exception. */ - public function testCreateDuplicateName() + public function testCreateDuplicateName(): void { $this->exec('bake migration CreateUsers --connection test'); $this->exec('bake migration CreateUsers --connection test'); @@ -144,7 +144,7 @@ public function testCreateDuplicateName() /** * Tests that baking a migration with the name as reserved keyword triggers prefixing. */ - public function testCreateWithReservedKeyword() + public function testCreateWithReservedKeyword(): void { $this->exec('bake migration New --connection test', ['Prefix']); @@ -152,7 +152,7 @@ public function testCreateWithReservedKeyword() $this->assertOutputRegExp('/Wrote.*?PrefixNew\.php/'); } - public function testCreateBuiltInAlias() + public function testCreateBuiltInAlias(): void { $this->exec('migrations create CreateUsers --connection test'); $this->assertExitCode(BaseCommand::CODE_SUCCESS); @@ -162,7 +162,7 @@ public function testCreateBuiltInAlias() /** * Tests that baking a migration with the "drop" string inside the name generates a valid drop migration. */ - public function testCreateDropMigration() + public function testCreateDropMigration(): void { $this->exec('bake migration DropUsers --connection test'); $this->assertOutputRegExp('/Wrote.*?DropUsers\.php/'); @@ -178,7 +178,7 @@ public function testCreateDropMigration() /** * Tests that baking a migration with the name as another with the parameter "force", will delete the existing file. */ - public function testCreateDuplicateNameWithForce() + public function testCreateDuplicateNameWithForce(): void { $this->exec('bake migration CreateUsers --connection test --force'); @@ -197,7 +197,7 @@ public function testCreateDuplicateNameWithForce() * * @return void */ - public function testAddPrimaryKeyToExistingTable() + public function testAddPrimaryKeyToExistingTable(): void { $this->exec('bake migration AddPkToUsers somefield:primary_key --connection test'); @@ -211,7 +211,7 @@ public function testAddPrimaryKeyToExistingTable() * * @return void */ - public function testAddPrimaryKeyToExistingUsersTable() + public function testAddPrimaryKeyToExistingUsersTable(): void { $this->exec('bake migration AlterUsers somefield:primary_key --connection test'); @@ -222,7 +222,7 @@ public function testAddPrimaryKeyToExistingUsersTable() /** * @return void */ - public function testDetectAction() + public function testDetectAction(): void { $command = new BakeMigrationCommand(); $this->assertEquals( @@ -382,7 +382,7 @@ public function testDetectAction() * * @return void */ - public function testActionWithoutValidPrefix() + public function testActionWithoutValidPrefix(): void { $this->exec('bake migration SleepUsers name:string --connection test'); @@ -395,7 +395,7 @@ public function testActionWithoutValidPrefix() * * @return void */ - public function testCreateAnonymousStyle() + public function testCreateAnonymousStyle(): void { $this->exec('bake migration CreateUsers name:string --style=anonymous --connection test'); @@ -403,7 +403,7 @@ public function testCreateAnonymousStyle() $this->assertCount(1, $files); $filePath = current($files); - $fileName = basename($filePath); + $fileName = basename((string)$filePath); // Check the file name format $this->assertMatchesRegularExpression('/^\d{4}_\d{2}_\d{2}_\d{6}_CreateUsers\.php$/', $fileName); @@ -422,7 +422,7 @@ public function testCreateAnonymousStyle() * * @return void */ - public function testCreateAnonymousStyleWithConfigure() + public function testCreateAnonymousStyleWithConfigure(): void { Configure::write('Migrations.style', 'anonymous'); @@ -432,7 +432,7 @@ public function testCreateAnonymousStyleWithConfigure() $this->assertCount(1, $files); $filePath = current($files); - $fileName = basename($filePath); + $fileName = basename((string)$filePath); // Check the file name format $this->assertMatchesRegularExpression('/^\d{4}_\d{2}_\d{2}_\d{6}_CreateUsers\.php$/', $fileName); @@ -451,7 +451,7 @@ public function testCreateAnonymousStyleWithConfigure() * * @return void */ - public function testCreateWithReferences() + public function testCreateWithReferences(): void { $this->exec('bake migration CreatePosts title:string user_id:references --connection test'); @@ -468,7 +468,7 @@ public function testCreateWithReferences() * * @return void */ - public function testCreateWithReferencesCustomTable() + public function testCreateWithReferencesCustomTable(): void { $this->exec('bake migration CreateArticles title:string author_id:references:authors --connection test'); @@ -485,7 +485,7 @@ public function testCreateWithReferencesCustomTable() * * @return void */ - public function testAddFieldWithReference() + public function testAddFieldWithReference(): void { $this->exec('bake migration AddCategoryIdToProducts category_id:references --connection test'); @@ -497,10 +497,10 @@ public function testAddFieldWithReference() $this->assertSameAsFile(__FUNCTION__ . '.php', $result); } - public function testBakeMigrationWithoutBake() + public function testBakeMigrationWithoutBake(): void { // Make sure to unload the Bake plugin - $this->createApp()->getEventManager()->on('Console.buildCommands', function ($event, $commands) { + $this->createApp()->getEventManager()->on('Console.buildCommands', function ($event, $commands): void { Plugin::getCollection()->remove('Bake'); }); diff --git a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php index 963571e17..0309c92af 100644 --- a/tests/TestCase/Command/BakeMigrationDiffCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationDiffCommandTest.php @@ -49,7 +49,7 @@ class BakeMigrationDiffCommandTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -82,13 +82,13 @@ public function setUp(): void try { $connection = ConnectionManager::get('test_comparisons'); $connection->execute('DROP TABLE IF EXISTS test_decimal_types'); - } catch (Exception $e) { + } catch (Exception) { // Ignore errors if connection doesn't exist yet } } } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); foreach ($this->generatedFiles as $file) { @@ -121,7 +121,7 @@ public function tearDown(): void $connection = ConnectionManager::get('test_comparisons'); $tables = ['articles', 'categories', 'comments', 'users', 'orphan_table', 'phinxlog', 'cake_migrations', 'tags', 'test_blog_phinxlog', 'test_decimal_types']; foreach ($tables as $table) { - $connection->execute("DROP TABLE IF EXISTS $table"); + $connection->execute('DROP TABLE IF EXISTS ' . $table); } Cache::clear('_cake_model_'); } @@ -132,7 +132,7 @@ public function tearDown(): void * * @return void */ - public function testHistoryNotInSync() + public function testHistoryNotInSync(): void { $expectedMessage = 'Your migrations history is not in sync with your migrations files. ' . 'Make sure all your migrations have been migrated before baking a diff.'; @@ -195,7 +195,7 @@ public function testCheckSyncWithPluginMigrationsMoreRecent(): void * * @return void */ - public function testEmptyHistoryNoMigrations() + public function testEmptyHistoryNoMigrations(): void { $this->exec('bake migration_diff EmptyHistoryNoMigrations -c test -p Blog'); @@ -215,7 +215,7 @@ public function testEmptyHistoryNoMigrations() * * @return void */ - public function testBakeMigrationDiffInCustomFolder() + public function testBakeMigrationDiffInCustomFolder(): void { $customFolderName = 'CustomMigrationsFolder'; $this->exec('bake migration_diff MigrationDiffForCustomFolder -c test -s ' . $customFolderName); @@ -237,7 +237,7 @@ public function testBakeMigrationDiffInCustomFolder() * * @return void */ - public function testBakeMigrationDiffGenerateOnly() + public function testBakeMigrationDiffGenerateOnly(): void { //$this->skipIf(!env('DB_URL_COMPARE')); @@ -276,7 +276,7 @@ public function testBakeMigrationDiffGenerateOnly() * * @return void */ - public function testBakingDiff() + public function testBakingDiff(): void { $this->skipIf(!env('DB_URL_COMPARE')); @@ -292,7 +292,7 @@ public function testBakingDiff() * * @return void */ - public function testBakingDiffSimple() + public function testBakingDiffSimple(): void { $this->skipIf(!env('DB_URL_COMPARE')); @@ -305,7 +305,7 @@ public function testBakingDiffSimple() * * @return void */ - public function testBakingDiffAddRemove() + public function testBakingDiffAddRemove(): void { $this->skipIf(!env('DB_URL_COMPARE')); @@ -462,11 +462,11 @@ protected function runDiffBakingTest(string $scenario): void $diffMigrationsPath = $diffConfigFolder . $prefix . Inflector::underscore($scenario) . '_' . $db . '.php'; $diffDumpPath = $diffConfigFolder . 'schema-dump-test_comparisons_' . $db . '.lock'; - $destinationConfigDir = ROOT . DS . 'config' . DS . "MigrationsDiff{$scenario}" . DS; + $destinationConfigDir = ROOT . DS . 'config' . DS . 'MigrationsDiff' . $scenario . DS; if (!is_dir($destinationConfigDir)) { mkdir($destinationConfigDir, 0777, true); } - $destination = $destinationConfigDir . "20160415220805_{$classPrefix}{$scenario}" . ucfirst($db) . '.php'; + $destination = $destinationConfigDir . sprintf('20160415220805_%s%s', $classPrefix, $scenario) . ucfirst($db) . '.php'; $destinationDumpPath = $destinationConfigDir . 'schema-dump-test_comparisons_' . $db . '.lock'; copy($diffMigrationsPath, $destination); @@ -475,7 +475,7 @@ protected function runDiffBakingTest(string $scenario): void $destinationDumpPath, ]; - $migrations = $this->getMigrations("MigrationsDiff$scenario"); + $migrations = $this->getMigrations('MigrationsDiff' . $scenario); $migrations->migrate(); copy($diffDumpPath, $destinationDumpPath); @@ -493,14 +493,14 @@ protected function runDiffBakingTest(string $scenario): void $this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Diff' . DS . lcfirst($scenario) . DS; - $bakeName = $this->getBakeName("TheDiff{$scenario}"); - $targetFolder = "MigrationsDiff{$scenario}"; + $bakeName = $this->getBakeName('TheDiff' . $scenario); + $targetFolder = 'MigrationsDiff' . $scenario; $comparison = lcfirst($scenario); - $this->exec("custom bake migration_diff {$bakeName} -c test_comparisons --test-target-folder {$targetFolder} --comparison {$comparison}"); + $this->exec(sprintf('custom bake migration_diff %s -c test_comparisons --test-target-folder %s --comparison %s', $bakeName, $targetFolder, $comparison)); $this->generatedFiles[] = ROOT . DS . 'config' . DS . 'Migrations' . DS . 'schema-dump-test_comparisons.lock'; - $generatedMigration = $this->getGeneratedMigrationName($destinationConfigDir, "*TheDiff$scenario*"); + $generatedMigration = $this->getGeneratedMigrationName($destinationConfigDir, sprintf('*TheDiff%s*', $scenario)); $fileName = pathinfo($generatedMigration, PATHINFO_FILENAME); $this->assertOutputContains('Marking the migration ' . $fileName . ' as migrated...'); $this->assertOutputContains('Creating a dump of the new database state...'); @@ -525,7 +525,7 @@ protected function runDiffBakingTest(string $scenario): void ->into($schemaTable) ->values($values) ->execute(); - $this->getMigrations("MigrationsDiff{$scenario}")->rollback(['target' => 'all']); + $this->getMigrations('MigrationsDiff' . $scenario)->rollback(['target' => 'all']); } /** @@ -537,14 +537,17 @@ protected function getDbType(): string { $connection = ConnectionManager::get('test_comparisons'); $driver = $connection->getDriver(); - if ($driver instanceof Mysql) { return 'mysql'; - } elseif ($driver instanceof Postgres) { + } + if ($driver instanceof Postgres) { return 'pgsql'; - } elseif ($driver instanceof Sqlite) { + } + if ($driver instanceof Sqlite) { return 'sqlite'; - } elseif ($driver instanceof Sqlserver) { + } + + if ($driver instanceof Sqlserver) { return 'sqlserver'; } @@ -557,15 +560,14 @@ protected function getDbType(): string * @param string $name Name of the baked file, unaware of the DB environment * @return string Baked filename */ - public function getBakeName($name) + public function getBakeName(string $name): string { $db = getenv('DB'); if (!$db) { $db = $this->getDbType(); } - $name .= ucfirst($db); - return $name; + return $name . ucfirst($db); } /** @@ -575,15 +577,14 @@ public function getBakeName($name) * @param string $source Source folder where migrations are located * @return Migrations */ - protected function getMigrations($source = 'MigrationsDiff') + protected function getMigrations($source = 'MigrationsDiff'): Migrations { $params = [ 'connection' => 'test_comparisons', 'source' => $source, ]; - $migrations = new Migrations($params); - return $migrations; + return new Migrations($params); } /** @@ -608,7 +609,7 @@ public function assertSameAsFile(string $path, string $result): void * @param string $result Results generated by the test to be compared * @return void */ - public function assertCorrectSnapshot($bakeName, $result) + public function assertCorrectSnapshot($bakeName, string $result): void { $dbenv = getenv('DB'); if (!$dbenv) { @@ -629,10 +630,10 @@ public function assertCorrectSnapshot($bakeName, $result) * @param string $needle The filename pattern to find. * @return string */ - public function getGeneratedMigrationName($configDir, $needle) + public function getGeneratedMigrationName(string $configDir, string $needle): string { $files = glob($configDir . $needle); - $this->assertNotEmpty($files, "Could not find any files matching `{$needle}` in `{$configDir}`"); + $this->assertNotEmpty($files, sprintf('Could not find any files matching `%s` in `%s`', $needle, $configDir)); // Record the generated file so we can cleanup if the test fails. $this->generatedFiles[] = $files[0]; diff --git a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php index d90980c7c..ac28573bb 100644 --- a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php @@ -52,7 +52,7 @@ class BakeMigrationSnapshotCommandTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -68,7 +68,7 @@ public function setUp(): void * * @return void */ - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); ConnectionManager::drop('alternative'); @@ -85,7 +85,7 @@ public function tearDown(): void * * @return void */ - public function testNotEmptySnapshot() + public function testNotEmptySnapshot(): void { $this->runSnapshotTest('NotEmpty'); } @@ -95,10 +95,10 @@ public function testNotEmptySnapshot() * * @return void */ - public function testNotEmptySnapshotNoLock() + public function testNotEmptySnapshotNoLock(): void { $bakeName = $this->getBakeName('TestNotEmptySnapshot'); - $this->exec("bake migration_snapshot {$bakeName} -c test --no-lock"); + $this->exec(sprintf('bake migration_snapshot %s -c test --no-lock', $bakeName)); $generatedMigration = glob($this->migrationPath . '*_TestNotEmptySnapshot*.php'); $this->generatedFiles = $generatedMigration; @@ -115,14 +115,14 @@ public function testNotEmptySnapshotNoLock() * * @return void */ - public function testSnapshotGenerateOnly() + public function testSnapshotGenerateOnly(): void { if (file_exists($this->migrationPath . 'schema-dump-test.lock')) { unlink($this->migrationPath . 'schema-dump-test.lock'); } $bakeName = $this->getBakeName('TestGenerateOnlySnapshot'); - $this->exec("bake migration_snapshot {$bakeName} -c test --generate-only"); + $this->exec(sprintf('bake migration_snapshot %s -c test --generate-only', $bakeName)); $generatedMigration = glob($this->migrationPath . '*_TestGenerateOnlySnapshot*.php'); $this->generatedFiles = $generatedMigration; @@ -150,12 +150,12 @@ public function testSnapshotPostgresTimestampTzColumn(): void $scenario = 'PostgresTimestampTz'; - $bakeName = $this->getBakeName("TestSnapshot{$scenario}"); - $this->exec("bake migration_snapshot {$bakeName} -c test"); + $bakeName = $this->getBakeName('TestSnapshot' . $scenario); + $this->exec(sprintf('bake migration_snapshot %s -c test', $bakeName)); $connection->execute('DROP TABLE postgres_timestamp_tz'); - $generatedMigration = glob($this->migrationPath . "*_TestSnapshot{$scenario}*.php"); + $generatedMigration = glob($this->migrationPath . sprintf('*_TestSnapshot%s*.php', $scenario)); $this->generatedFiles = $generatedMigration; $this->generatedFiles[] = $this->migrationPath . 'schema-dump-test.lock'; @@ -172,7 +172,7 @@ public function testSnapshotPostgresTimestampTzColumn(): void * * @return void */ - public function testAutoIdDisabledSnapshot() + public function testAutoIdDisabledSnapshot(): void { $this->runSnapshotTest('AutoIdDisabled', '--disable-autoid'); } @@ -182,7 +182,7 @@ public function testAutoIdDisabledSnapshot() * * @return void */ - public function testSnapshotWithChange() + public function testSnapshotWithChange(): void { $this->runSnapshotTest('WithChange', '--change'); } @@ -247,7 +247,7 @@ public function testSnapshotDiffWithAutoIdIncompatibleUnsignedPrimaryKeys(): voi * * @return void */ - public function testPluginBlog() + public function testPluginBlog(): void { $this->loadPlugins(['TestBlog']); $this->migrationPath = ROOT . DS . 'Plugin' . DS . 'TestBlog' . DS . 'config' . DS . 'Migrations' . DS; @@ -263,13 +263,13 @@ public function testPluginBlog() * * @return void */ - public function testPluginWithCustomConnection() + public function testPluginWithCustomConnection(): void { $this->loadPlugins(['SimpleSnapshot']); $this->migrationPath = ROOT . DS . 'Plugin' . DS . 'SimpleSnapshot' . DS . 'config' . DS . 'Migrations' . DS; $bakeName = $this->getBakeName('TestSnapshotPluginCustomConnection'); - $this->exec("bake migration_snapshot {$bakeName} -c test -p SimpleSnapshot"); + $this->exec(sprintf('bake migration_snapshot %s -c test -p SimpleSnapshot', $bakeName)); $generatedMigration = glob($this->migrationPath . '*_TestSnapshotPluginCustomConnection*.php'); $this->generatedFiles = $generatedMigration; @@ -286,13 +286,13 @@ public function testPluginWithCustomConnection() protected function runSnapshotTest(string $scenario, string $arguments = ''): void { if ($arguments) { - $arguments = " $arguments"; + $arguments = ' ' . $arguments; } - $bakeName = $this->getBakeName("TestSnapshot{$scenario}"); - $this->exec("bake migration_snapshot {$bakeName} -c test{$arguments}"); + $bakeName = $this->getBakeName('TestSnapshot' . $scenario); + $this->exec(sprintf('bake migration_snapshot %s -c test%s', $bakeName, $arguments)); - $generatedMigration = glob($this->migrationPath . "*_TestSnapshot{$scenario}*.php"); + $generatedMigration = glob($this->migrationPath . sprintf('*_TestSnapshot%s*.php', $scenario)); $this->generatedFiles = $generatedMigration; $this->generatedFiles[] = $this->migrationPath . 'schema-dump-test.lock'; @@ -310,7 +310,7 @@ protected function runSnapshotTest(string $scenario, string $arguments = ''): vo * @param string $name Name of the baked file, unaware of the DB environment * @return string Baked filename */ - public function getBakeName($name) + public function getBakeName(string $name) { $dbenv = getenv('DB'); if ($dbenv !== 'mysql') { @@ -348,6 +348,7 @@ public function assertSameAsFile(string $path, string $result): void // Normalize utf8mb3 to utf8 for MySQL 8.0.30+ compatibility $expected = str_replace('utf8mb3_', 'utf8_', $expected); + $result = str_replace('utf8mb3_', 'utf8_', $result); // Normalize unified table name to legacy for comparison @@ -363,7 +364,7 @@ public function assertSameAsFile(string $path, string $result): void * @param string $result Results generated by the test to be compared * @return void */ - public function assertCorrectSnapshot($bakeName, $result) + public function assertCorrectSnapshot($bakeName, string $result): void { $dbenv = getenv('DB'); $bakeName = Inflector::underscore($bakeName); diff --git a/tests/TestCase/Command/BakeSeedCommandTest.php b/tests/TestCase/Command/BakeSeedCommandTest.php index a44d2c514..f28285ee6 100644 --- a/tests/TestCase/Command/BakeSeedCommandTest.php +++ b/tests/TestCase/Command/BakeSeedCommandTest.php @@ -46,7 +46,7 @@ class BakeSeedCommandTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->_compareBasePath = Plugin::path('Migrations') . 'tests' . DS . 'comparisons' . DS . 'Seeds' . DS; @@ -57,7 +57,7 @@ public function setUp(): void * * @return void */ - public function testBasicBaking() + public function testBasicBaking(): void { $this->generatedFile = ROOT . DS . 'config/Seeds/ArticlesSeed.php'; $this->exec('bake seed Articles --connection test'); @@ -72,7 +72,7 @@ public function testBasicBaking() * * @return void */ - public function testBakeWithApplicationTemplate() + public function testBakeWithApplicationTemplate(): void { copy( ROOT . '/App/Template/plugin/Migrations/bake/Seed/custom-seed.twig', @@ -93,13 +93,13 @@ public function testBakeWithApplicationTemplate() * * @return void */ - public function testWithData() + public function testWithData(): void { $this->generatedFile = ROOT . DS . 'config/Seeds/EventsSeed.php'; $this->exec('bake seed Events --connection test --data'); $path = __FUNCTION__ . '.php'; - if (in_array(getenv('DB'), ['pgsql', 'sqlserver'])) { + if (in_array(getenv('DB'), ['pgsql', 'sqlserver'], true)) { $path = getenv('DB') . DS . $path; } elseif (PHP_VERSION_ID >= 80100) { $path = 'php81' . DS . $path; @@ -115,7 +115,7 @@ public function testWithData() * * @return void */ - public function testWithDataAndFields() + public function testWithDataAndFields(): void { $this->generatedFile = ROOT . DS . 'config/Seeds/EventsSeed.php'; $this->exec('bake seed Events --connection test --data --fields title,description'); @@ -130,13 +130,13 @@ public function testWithDataAndFields() * * @return void */ - public function testWithDataAndLimit() + public function testWithDataAndLimit(): void { $this->generatedFile = ROOT . DS . 'config/Seeds/EventsSeed.php'; $this->exec('bake seed Events --connection test --data --limit 2'); $path = __FUNCTION__ . '.php'; - if (in_array(getenv('DB'), ['pgsql', 'sqlserver'])) { + if (in_array(getenv('DB'), ['pgsql', 'sqlserver'], true)) { $path = getenv('DB') . DS . $path; } elseif (PHP_VERSION_ID >= 80100) { $path = 'php81' . DS . $path; @@ -152,7 +152,7 @@ public function testWithDataAndLimit() * * @return void */ - public function testPrettifyArray() + public function testPrettifyArray(): void { $this->generatedFile = ROOT . DS . 'config/Seeds/TextsSeed.php'; $this->exec('bake seed Texts --connection test --data'); @@ -167,7 +167,7 @@ public function testPrettifyArray() * * @return void */ - public function testAnonymousStyleWithConfigure() + public function testAnonymousStyleWithConfigure(): void { Configure::write('Migrations.style', 'anonymous'); diff --git a/tests/TestCase/Command/CompletionTest.php b/tests/TestCase/Command/CompletionTest.php index b05f0a4da..94cf1f9cd 100644 --- a/tests/TestCase/Command/CompletionTest.php +++ b/tests/TestCase/Command/CompletionTest.php @@ -24,12 +24,14 @@ class CompletionTest extends TestCase { use ConsoleIntegrationTestTrait; + public $Shell; + /** * tearDown * * @return void */ - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); unset($this->Shell); @@ -41,7 +43,7 @@ public function tearDown(): void * * @return void */ - public function testMigrationsSubcommands() + public function testMigrationsSubcommands(): void { $this->exec('completion subcommands migrations.migrations'); // Upgrade command is hidden when legacyTables is disabled @@ -64,7 +66,7 @@ public function testMigrationsSubcommands() * * @return void */ - public function testMigrationsOptionsMarkMigrated() + public function testMigrationsOptionsMarkMigrated(): void { $this->exec('completion options migrations.migrations mark_migrated'); $this->assertCount(1, $this->_out->messages()); @@ -85,7 +87,7 @@ public function testMigrationsOptionsMarkMigrated() * * @return void */ - public function testMigrationsOptionsMigrate() + public function testMigrationsOptionsMigrate(): void { $this->exec('completion options migrations.migrations migrate'); $this->assertCount(1, $this->_out->messages()); @@ -106,7 +108,7 @@ public function testMigrationsOptionsMigrate() * * @return void */ - public function testMigrationsOptionsRollback() + public function testMigrationsOptionsRollback(): void { $this->exec('completion options migrations.migrations rollback'); $this->assertCount(1, $this->_out->messages()); @@ -127,7 +129,7 @@ public function testMigrationsOptionsRollback() * * @return void */ - public function testMigrationsOptionsStatus() + public function testMigrationsOptionsStatus(): void { $this->exec('completion options migrations.migrations status'); $this->assertCount(1, $this->_out->messages()); diff --git a/tests/TestCase/Command/DumpCommandTest.php b/tests/TestCase/Command/DumpCommandTest.php index fe19b4072..a666415c6 100644 --- a/tests/TestCase/Command/DumpCommandTest.php +++ b/tests/TestCase/Command/DumpCommandTest.php @@ -14,10 +14,12 @@ class DumpCommandTest extends TestCase { protected Connection $connection; + protected string $_compareBasePath; + protected string $dumpFile; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -32,7 +34,7 @@ public function setUp(): void $this->dumpFile = ROOT . DS . 'config/TestsMigrations/schema-dump-test.lock'; } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); diff --git a/tests/TestCase/Command/EntryCommandTest.php b/tests/TestCase/Command/EntryCommandTest.php index 112a78454..d63aed559 100644 --- a/tests/TestCase/Command/EntryCommandTest.php +++ b/tests/TestCase/Command/EntryCommandTest.php @@ -26,7 +26,7 @@ class EntryCommandTest extends TestCase { use ConsoleIntegrationTestTrait; - public function setUp(): void + protected function setUp(): void { parent::setUp(); } @@ -36,7 +36,7 @@ public function setUp(): void * * @return void */ - public function testExecuteHelp() + public function testExecuteHelp(): void { $this->exec('migrations --help'); @@ -51,7 +51,7 @@ public function testExecuteHelp() * * @return void */ - public function testExecuteMissingCommand() + public function testExecuteMissingCommand(): void { $this->exec('migrations derp'); diff --git a/tests/TestCase/Command/MarkMigratedTest.php b/tests/TestCase/Command/MarkMigratedTest.php index 669f19327..a417a2908 100644 --- a/tests/TestCase/Command/MarkMigratedTest.php +++ b/tests/TestCase/Command/MarkMigratedTest.php @@ -34,7 +34,7 @@ class MarkMigratedTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -51,7 +51,7 @@ public function setUp(): void * * @return void */ - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); $this->connection->execute('DROP TABLE IF EXISTS migrator_phinxlog'); @@ -65,7 +65,7 @@ public function tearDown(): void * * @return void */ - public function testExecute() + public function testExecute(): void { $this->exec('migrations mark_migrated --connection=test --source=TestsMigrations'); @@ -102,7 +102,7 @@ public function testExecute() $this->assertEquals(4, $result->fetchColumn(0)); } - public function testExecuteTarget() + public function testExecuteTarget(): void { $this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --target=20150704160200'); $this->assertExitSuccess(); @@ -157,7 +157,7 @@ public function testTargetNotFound(): void ); } - public function testExecuteTargetWithExclude() + public function testExecuteTargetWithExclude(): void { $this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --target=20150724233100 --exclude'); $this->assertExitSuccess(); @@ -206,7 +206,7 @@ public function testExecuteTargetWithExcludeNotFound(): void ); } - public function testExecuteTargetWithOnly() + public function testExecuteTargetWithOnly(): void { $this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --target=20150724233100 --only'); $this->assertExitSuccess(); @@ -252,7 +252,7 @@ public function testExecuteTargetWithOnlyNotFound(): void ); } - public function testExecuteInvalidUseOfExclude() + public function testExecuteInvalidUseOfExclude(): void { $this->exec('migrations mark_migrated --connection=test --source=TestsMigrations --exclude'); @@ -287,7 +287,7 @@ public function testExecutePluginInvalid(): void try { $this->exec('migrations mark_migrated -c test --plugin NotThere'); $this->fail('Should raise an error or exit with an error'); - } catch (MissingPluginException $e) { + } catch (MissingPluginException) { $this->assertTrue(true); } /** @var \Cake\Database\Connection $connection */ diff --git a/tests/TestCase/Command/MigrateCommandTest.php b/tests/TestCase/Command/MigrateCommandTest.php index 8b063afc6..5b2dca1e8 100644 --- a/tests/TestCase/Command/MigrateCommandTest.php +++ b/tests/TestCase/Command/MigrateCommandTest.php @@ -13,7 +13,7 @@ class MigrateCommandTest extends TestCase { protected array $createdFiles = []; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -21,7 +21,7 @@ public function setUp(): void $this->clearMigrationRecords('test', 'Migrator'); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); foreach ($this->createdFiles as $file) { @@ -30,7 +30,7 @@ public function tearDown(): void ConnectionManager::drop('invalid'); } - public function testHelp() + public function testHelp(): void { $this->exec('migrations migrate --help'); @@ -127,7 +127,7 @@ public function testMigrateWithSourceMigration(): void /** * Test dry-run */ - public function testMigrateDryRun() + public function testMigrateDryRun(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --dry-run'); @@ -146,7 +146,7 @@ public function testMigrateDryRun() /** * Test that migrations only run to a certain date */ - public function testMigrateDate() + public function testMigrateDate(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --date 2020-01-01'); @@ -162,7 +162,7 @@ public function testMigrateDate() /** * Test output for dates with no matching migrations */ - public function testMigrateDateNotFound() + public function testMigrateDateNotFound(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --date 2000-01-01'); @@ -179,7 +179,7 @@ public function testMigrateDateNotFound() /** * Test advancing migrations with an offset. */ - public function testMigrateTarget() + public function testMigrateTarget(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --target 20150416223600'); @@ -196,7 +196,7 @@ public function testMigrateTarget() $this->assertFileExists($dumpFile); } - public function testMigrateTargetNotFound() + public function testMigrateTargetNotFound(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --target 99'); @@ -214,7 +214,7 @@ public function testMigrateTargetNotFound() $this->assertFileExists($dumpFile); } - public function testMigrateFakeAll() + public function testMigrateFakeAll(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --fake'); @@ -232,7 +232,7 @@ public function testMigrateFakeAll() $this->assertFileExists($dumpFile); } - public function testMigratePlugin() + public function testMigratePlugin(): void { $this->loadPlugins(['Migrator']); $migrationPath = ROOT . DS . 'Plugin' . DS . 'Migrator' . DS . 'config' . DS . 'Migrations'; @@ -250,12 +250,12 @@ public function testMigratePlugin() $this->assertFileExists($dumpFile); } - public function testMigratePluginInvalid() + public function testMigratePluginInvalid(): void { try { $this->exec('migrations migrate -c test --plugin NotThere'); $this->fail('Should raise an error or exit with an error'); - } catch (MissingPluginException $e) { + } catch (MissingPluginException) { $this->assertTrue(true); } @@ -270,7 +270,7 @@ public function testMigratePluginInvalid() * * @return void */ - public function testMigrateWithNoLock() + public function testMigrateWithNoLock(): void { $migrationPath = ROOT . DS . 'config' . DS . 'Migrations'; $this->exec('migrations migrate -c test --no-lock'); diff --git a/tests/TestCase/Command/RollbackCommandTest.php b/tests/TestCase/Command/RollbackCommandTest.php index da94d200c..e24fdc250 100644 --- a/tests/TestCase/Command/RollbackCommandTest.php +++ b/tests/TestCase/Command/RollbackCommandTest.php @@ -3,6 +3,7 @@ namespace Migrations\Test\TestCase\Command; +use Cake\Console\TestSuite\StubConsoleOutput; use Cake\Datasource\ConnectionManager; use Cake\Event\EventInterface; use Cake\Event\EventManager; @@ -14,7 +15,7 @@ class RollbackCommandTest extends TestCase { protected array $createdFiles = []; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -22,7 +23,7 @@ public function setUp(): void $this->clearMigrationRecords('test', 'Migrator'); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); foreach ($this->createdFiles as $file) { @@ -32,7 +33,7 @@ public function tearDown(): void protected function resetOutput(): void { - if ($this->_out) { + if ($this->_out instanceof StubConsoleOutput) { $property = new ReflectionProperty($this->_out, '_out'); $property->setValue($this->_out, []); } diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php index e401e460b..f98e691d8 100644 --- a/tests/TestCase/Command/SeedCommandTest.php +++ b/tests/TestCase/Command/SeedCommandTest.php @@ -11,14 +11,14 @@ class SeedCommandTest extends TestCase { - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->clearMigrationRecords('test'); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); /** @var \Cake\Database\Connection $connection */ @@ -543,7 +543,7 @@ public function testIdempotentSeed(): void $this->assertEquals(2, $query->fetchColumn(0)); // Verify the seed WAS tracked in cake_seeds table (only one record, updated each run) - $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\''); + $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'IdempotentTestSeed'"); $this->assertEquals(1, $seedLog->fetchColumn(0), 'Idempotent seeds should track last execution'); } @@ -560,7 +560,7 @@ public function testNonIdempotentSeedIsTracked(): void $connection = ConnectionManager::get('test'); // Verify the seed WAS tracked in cake_seeds table - $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'"); $this->assertEquals(1, $seedLog->fetchColumn(0), 'Regular seeds should be tracked'); // Run again - should be silently skipped @@ -589,7 +589,7 @@ public function testFakeSeedMarksAsExecuted(): void $this->assertEquals(0, $query->fetchColumn(0), 'Fake seed should not insert data'); // Verify the seed WAS tracked in cake_seeds table - $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'"); $this->assertEquals(1, $seedLog->fetchColumn(0), 'Fake seeds should be tracked'); // Running again should be silently skipped @@ -610,7 +610,7 @@ public function testFakeSeedWithForce(): void $this->assertExitSuccess(); // Verify seed is tracked - $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'"); $this->assertEquals(1, $seedLog->fetchColumn(0)); // Run with --force to actually execute it @@ -638,10 +638,10 @@ public function testResetSpecificSeed(): void $this->assertExitSuccess(); // Verify both are tracked - $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $numbersLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'"); $this->assertEquals(1, $numbersLog->fetchColumn(0)); - $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\''); + $storesLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'StoresSeed'"); $this->assertEquals(1, $storesLog->fetchColumn(0)); // Reset only Numbers seed @@ -651,10 +651,10 @@ public function testResetSpecificSeed(): void $this->assertOutputNotContains('All seeds will be reset:'); // Verify Numbers is reset but Stores is still tracked - $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $numbersLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'"); $this->assertEquals(0, $numbersLog->fetchColumn(0), 'Numbers seed should be reset'); - $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\''); + $storesLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'StoresSeed'"); $this->assertEquals(1, $storesLog->fetchColumn(0), 'Stores seed should still be tracked'); } @@ -674,10 +674,10 @@ public function testResetMultipleSpecificSeeds(): void $this->assertExitSuccess(); // Verify both are reset - $numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\''); + $numbersLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'NumbersSeed'"); $this->assertEquals(0, $numbersLog->fetchColumn(0)); - $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\''); + $storesLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'StoresSeed'"); $this->assertEquals(0, $storesLog->fetchColumn(0)); } @@ -708,7 +708,7 @@ public function testFakeIdempotentSeedIsTracked(): void $this->assertEquals(0, $query->fetchColumn(0), 'Fake seed should not insert data'); // Verify the seed WAS tracked - $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\''); + $seedLog = $connection->execute("SELECT COUNT(*) FROM cake_seeds WHERE seed_name = 'IdempotentTestSeed'"); $this->assertEquals(1, $seedLog->fetchColumn(0), 'Idempotent seeds should be tracked when faked'); } } diff --git a/tests/TestCase/Command/StatusCommandTest.php b/tests/TestCase/Command/StatusCommandTest.php index 48acaf229..6b3b2f7e1 100644 --- a/tests/TestCase/Command/StatusCommandTest.php +++ b/tests/TestCase/Command/StatusCommandTest.php @@ -3,13 +3,14 @@ namespace Migrations\Test\TestCase\Command; +use Cake\Console\TestSuite\StubConsoleOutput; use Cake\Core\Exception\MissingPluginException; use Migrations\Test\TestCase\TestCase; use RuntimeException; class StatusCommandTest extends TestCase { - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -41,7 +42,7 @@ public function testExecuteSimpleJson(): void $this->exec('migrations status -c test --format json'); $this->assertExitSuccess(); - assert(isset($this->_out)); + assert($this->_out instanceof StubConsoleOutput); $output = $this->_out->messages(); $parsed = json_decode($output[0], true); $this->assertTrue(is_array($parsed)); diff --git a/tests/TestCase/Command/UpgradeCommandTest.php b/tests/TestCase/Command/UpgradeCommandTest.php index c7900bd7a..3a3890229 100644 --- a/tests/TestCase/Command/UpgradeCommandTest.php +++ b/tests/TestCase/Command/UpgradeCommandTest.php @@ -12,7 +12,7 @@ class UpgradeCommandTest extends TestCase { - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -23,7 +23,7 @@ public function setUp(): void $connection->execute('DROP TABLE IF EXISTS cake_migrations'); } - public function tearDown(): void + protected function tearDown(): void { $this->clearMigrationRecords('test'); @@ -48,7 +48,7 @@ protected function getAdapter(): AdapterInterface public function testHelp(): void { - Configure::write('Migrations.legacyTables', null); + Configure::write('Migrations.legacyTables'); $this->exec('migrations upgrade --help'); $this->assertExitSuccess(); @@ -61,7 +61,7 @@ public function testExecuteSimpleDryRun(): void Configure::write('Migrations.legacyTables', true); try { $this->getAdapter()->createSchemaTable(); - } catch (Exception $e) { + } catch (Exception) { // Table probably exists } @@ -85,7 +85,7 @@ public function testExecuteSimpleExecute(): void $adapter = $environment->getAdapter(); try { $adapter->createSchemaTable(); - } catch (Exception $e) { + } catch (Exception) { // Table probably exists } @@ -113,7 +113,7 @@ public function testExecuteSimpleExecuteDropTables(): void $adapter = $environment->getAdapter(); try { $adapter->createSchemaTable(); - } catch (Exception $e) { + } catch (Exception) { // Table probably exists } @@ -135,7 +135,7 @@ public function testExecuteWithMigrations(): void Configure::write('Migrations.legacyTables', true); try { $this->getAdapter()->createSchemaTable(); - } catch (Exception $e) { + } catch (Exception) { // Table probably exists } @@ -185,7 +185,7 @@ public function testExecuteWithSlashInPluginName(): void $adapter = $environment->getAdapter(); try { $adapter->createSchemaTable(); - } catch (Exception $e) { + } catch (Exception) { // Table probably exists } diff --git a/tests/TestCase/Config/ConfigMigrationPathsTest.php b/tests/TestCase/Config/ConfigMigrationPathsTest.php index 8a1165ba8..a849eb7c9 100644 --- a/tests/TestCase/Config/ConfigMigrationPathsTest.php +++ b/tests/TestCase/Config/ConfigMigrationPathsTest.php @@ -10,7 +10,7 @@ */ class ConfigMigrationPathsTest extends AbstractConfigTestCase { - public function testGetMigrationPathsThrowsExceptionForNoPath() + public function testGetMigrationPathsThrowsExceptionForNoPath(): void { $config = new Config([]); @@ -22,7 +22,7 @@ public function testGetMigrationPathsThrowsExceptionForNoPath() /** * Normal behavior */ - public function testGetMigrationPaths() + public function testGetMigrationPaths(): void { $config = new Config($this->getConfigArray()); $this->assertEquals($this->getMigrationPath(), $config->getMigrationPath()); diff --git a/tests/TestCase/Config/ConfigSeedPathsTest.php b/tests/TestCase/Config/ConfigSeedPathsTest.php index 8697979bf..164c8404b 100644 --- a/tests/TestCase/Config/ConfigSeedPathsTest.php +++ b/tests/TestCase/Config/ConfigSeedPathsTest.php @@ -10,7 +10,7 @@ */ class ConfigSeedPathsTest extends AbstractConfigTestCase { - public function testGetSeedPathsThrowsExceptionForNoPath() + public function testGetSeedPathsThrowsExceptionForNoPath(): void { $config = new Config([]); @@ -22,13 +22,13 @@ public function testGetSeedPathsThrowsExceptionForNoPath() /** * Normal behavior */ - public function testGetSeedPaths() + public function testGetSeedPaths(): void { $config = new Config($this->getConfigArray()); $this->assertEquals($this->getSeedPath(), $config->getSeedPath()); } - public function testGetSeedPathConvertsStringToArray() + public function testGetSeedPathConvertsStringToArray(): void { $values = [ 'paths' => [ diff --git a/tests/TestCase/Config/ConfigTest.php b/tests/TestCase/Config/ConfigTest.php index 9197956be..5bd0ba4c7 100644 --- a/tests/TestCase/Config/ConfigTest.php +++ b/tests/TestCase/Config/ConfigTest.php @@ -12,14 +12,14 @@ */ class ConfigTest extends AbstractConfigTestCase { - public function testGetEnvironmentMethod() + public function testGetEnvironmentMethod(): void { $config = new Config($this->getConfigArray()); $db = $config->getEnvironment(); $this->assertArrayHasKey('adapter', $db); } - public function testEnvironmentHasMigrationTable() + public function testEnvironmentHasMigrationTable(): void { $configArray = $this->getConfigArray(); $configArray['environment']['migration_table'] = 'test_table'; @@ -28,7 +28,7 @@ public function testEnvironmentHasMigrationTable() $this->assertSame('test_table', $config->getEnvironment()['migration_table']); } - public function testArrayAccessMethods() + public function testArrayAccessMethods(): void { $config = new Config([]); $config['foo'] = 'bar'; @@ -38,7 +38,7 @@ public function testArrayAccessMethods() $this->assertArrayNotHasKey('foo', $config); } - public function testUndefinedArrayAccess() + public function testUndefinedArrayAccess(): void { $config = new Config([]); @@ -48,7 +48,7 @@ public function testUndefinedArrayAccess() $config['foo']; } - public function testGetSeedPath() + public function testGetSeedPath(): void { $config = new Config(['paths' => ['seeds' => 'db/seeds']]); $this->assertEquals('db/seeds', $config->getSeedPath()); @@ -57,7 +57,7 @@ public function testGetSeedPath() $this->assertEquals('db/seeds1', $config->getSeedPath()); } - public function testGetSeedPathThrowsException() + public function testGetSeedPathThrowsException(): void { $config = new Config([]); @@ -67,7 +67,7 @@ public function testGetSeedPathThrowsException() $config->getSeedPath(); } - public function testGetVersionOrder() + public function testGetVersionOrder(): void { $config = new Config([]); $config['version_order'] = Config::VERSION_ORDER_EXECUTION_TIME; @@ -75,7 +75,7 @@ public function testGetVersionOrder() } #[DataProvider('isVersionOrderCreationTimeDataProvider')] - public function testIsVersionOrderCreationTime($versionOrder, $expected) + public function testIsVersionOrderCreationTime(string $versionOrder, bool $expected): void { // get config stub $configStub = $this->getMockBuilder(Config::class) @@ -90,7 +90,7 @@ public function testIsVersionOrderCreationTime($versionOrder, $expected) $this->assertEquals($expected, $configStub->isVersionOrderCreationTime()); } - public static function isVersionOrderCreationTimeDataProvider() + public static function isVersionOrderCreationTimeDataProvider(): array { return [ 'With Creation Time Version Order' => diff --git a/tests/TestCase/Db/Adapter/AbstractAdapterTest.php b/tests/TestCase/Db/Adapter/AbstractAdapterTest.php index 425d5281d..5279b0705 100644 --- a/tests/TestCase/Db/Adapter/AbstractAdapterTest.php +++ b/tests/TestCase/Db/Adapter/AbstractAdapterTest.php @@ -18,10 +18,7 @@ class AbstractAdapterTest extends TestCase { - /** - * @var \Migrations\Db\Adapter\AbstractAdapter|\PHPUnit\Framework\MockObject\MockObject - */ - private $adapter; + private AbstractAdapter $adapter; protected function setUp(): void { @@ -35,14 +32,14 @@ protected function tearDown(): void unset($this->adapter); } - public function testOptions() + public function testOptions(): void { $options = $this->adapter->getOptions(); $this->assertArrayHasKey('foo', $options); $this->assertEquals('bar', $options['foo']); } - public function testOptionsSetSchemaTableName() + public function testOptionsSetSchemaTableName(): void { // When unified table mode is enabled, getSchemaTableName() returns cake_migrations $expectedDefault = Configure::read('Migrations.legacyTables') === false @@ -58,7 +55,7 @@ public function testOptionsSetSchemaTableName() $this->assertEquals($expectedAfterSet, $this->adapter->getSchemaTableName()); } - public function testSchemaTableName() + public function testSchemaTableName(): void { $expectedDefault = Configure::read('Migrations.legacyTables') === false ? UnifiedMigrationsTableStorage::TABLE_NAME @@ -71,7 +68,7 @@ public function testSchemaTableName() $this->assertEquals($expectedAfterSet, $this->adapter->getSchemaTableName()); } - public function testGetVersionLogInvalidVersionOrderKO() + public function testGetVersionLogInvalidVersionOrderKO(): void { $this->expectExceptionMessage('Invalid version_order configuration option'); $adapter = new class (['version_order' => 'invalid']) extends AbstractAdapter { @@ -83,7 +80,7 @@ public function testGetVersionLogInvalidVersionOrderKO() $adapter->getVersionLog(); } - public function testGetVersionLongDryRun() + public function testGetVersionLongDryRun(): void { $adapter = new class (['version_order' => Config::VERSION_ORDER_CREATION_TIME]) extends AbstractAdapter { use DefaultAdapterTrait; @@ -122,7 +119,7 @@ public function fetchAll(string $sql): array * @see https://github.com/cakephp/phinx/issues/1891 */ #[DataProvider('currentTimestampDefaultValueProvider')] - public function testCurrentTimestampDefaultValueQuoting($default, $columnType, $expected) + public function testCurrentTimestampDefaultValueQuoting(string|Literal|bool $default, ?string $columnType, string $expected): void { $adapter = new class (['version_order' => Config::VERSION_ORDER_CREATION_TIME]) extends AbstractAdapter { use DefaultAdapterTrait; diff --git a/tests/TestCase/Db/Adapter/AdapterFactoryTest.php b/tests/TestCase/Db/Adapter/AdapterFactoryTest.php index 8db24f3bf..e1fd15594 100644 --- a/tests/TestCase/Db/Adapter/AdapterFactoryTest.php +++ b/tests/TestCase/Db/Adapter/AdapterFactoryTest.php @@ -5,17 +5,18 @@ use Migrations\Db\Adapter\AbstractAdapter; use Migrations\Db\Adapter\AdapterFactory; +use Migrations\Db\Adapter\AdapterInterface; +use Migrations\Db\Adapter\MysqlAdapter; +use Migrations\Db\Adapter\TimedOutputAdapter; use Migrations\Test\TestCase\Db\Adapter\DefaultAdapterTrait; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use ReflectionMethod; use RuntimeException; class AdapterFactoryTest extends TestCase { - /** - * @var \Migrations\Db\Adapter\AdapterFactory - */ - private $factory; + private AdapterFactory $factory; protected function setUp(): void { @@ -27,17 +28,17 @@ protected function tearDown(): void unset($this->factory); } - public function testInstanceIsFactory() + public function testInstanceIsFactory(): void { $this->assertInstanceOf(AdapterFactory::class, $this->factory); } - public function testRegisterAdapter() + public function testRegisterAdapter(): void { $pdo = new class (['foo' => 'bar']) extends AbstractAdapter { use DefaultAdapterTrait; }; - $this->factory->registerAdapter('test', function (array $options) use ($pdo) { + $this->factory->registerAdapter('test', function (array $options) use ($pdo): object { $this->assertEquals('value', $options['key']); return $pdo; @@ -46,7 +47,7 @@ public function testRegisterAdapter() $this->assertEquals($pdo, $this->factory->getAdapter('test', ['key' => 'value'])); } - public function testRegisterAdapterFailure() + public function testRegisterAdapterFailure(): void { $adapter = static::class; @@ -56,14 +57,14 @@ public function testRegisterAdapterFailure() $this->factory->registerAdapter('test', $adapter); } - public function testGetAdapter() + public function testGetAdapter(): void { $adapter = $this->factory->getAdapter('mysql', []); - $this->assertInstanceOf('Migrations\Db\Adapter\MysqlAdapter', $adapter); + $this->assertInstanceOf(MysqlAdapter::class, $adapter); } - public function testGetAdapterFailure() + public function testGetAdapterFailure(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Adapter "bad" has not been registered'); @@ -71,12 +72,11 @@ public function testGetAdapterFailure() $this->factory->getAdapter('bad', []); } - public function testRegisterWrapper() + public function testRegisterWrapper(): void { // WrapperFactory::getClass is protected, work around it to avoid // creating unnecessary instances and making the test more complex. - $method = new ReflectionMethod(get_class($this->factory), 'getWrapperClass'); - $method->setAccessible(true); + $method = new ReflectionMethod($this->factory::class, 'getWrapperClass'); $wrapper = $method->invoke($this->factory, 'record'); $this->factory->registerWrapper('test', $wrapper); @@ -84,7 +84,7 @@ public function testRegisterWrapper() $this->assertEquals($wrapper, $method->invoke($this->factory, 'test')); } - public function testRegisterWrapperFailure() + public function testRegisterWrapperFailure(): void { $wrapper = static::class; @@ -94,19 +94,19 @@ public function testRegisterWrapperFailure() $this->factory->registerWrapper('test', $wrapper); } - private function getAdapterMock() + private function getAdapterMock(): MockObject { - return $this->getMockBuilder('Migrations\Db\Adapter\AdapterInterface')->getMock(); + return $this->getMockBuilder(AdapterInterface::class)->getMock(); } - public function testGetWrapper() + public function testGetWrapper(): void { $wrapper = $this->factory->getWrapper('timed', $this->getAdapterMock()); - $this->assertInstanceOf('Migrations\Db\Adapter\TimedOutputAdapter', $wrapper); + $this->assertInstanceOf(TimedOutputAdapter::class, $wrapper); } - public function testGetWrapperFailure() + public function testGetWrapperFailure(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Wrapper "nope" has not been registered'); diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 94202d83b..93f05d046 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -29,16 +29,12 @@ class MysqlAdapterTest extends TestCase { - /** - * @var \Migrations\Db\Adapter\MysqlAdapter - */ - private $adapter; + private MysqlAdapter $adapter; + + private array $config; - /** - * @var array - */ - private $config; private StubConsoleOutput $out; + private ConsoleIo $io; protected function setUp(): void @@ -102,12 +98,12 @@ private function usingMariaDbWithUuid(): bool return version_compare($version, '10.7.0', '>='); } - public function testConnection() + public function testConnection(): void { $this->assertInstanceOf(Connection::class, $this->adapter->getConnection()); } - public function testCreatingTheSchemaTableOnConnect() + public function testCreatingTheSchemaTableOnConnect(): void { $this->adapter->connect(); $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); @@ -118,7 +114,7 @@ public function testCreatingTheSchemaTableOnConnect() $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); } - public function testSchemaTableIsCreatedWithPrimaryKey() + public function testSchemaTableIsCreatedWithPrimaryKey(): void { // Skip for unified table mode since schema structure is different if (Configure::read('Migrations.legacyTables') === false) { @@ -130,7 +126,7 @@ public function testSchemaTableIsCreatedWithPrimaryKey() $this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version'])); } - public function testDatabaseNameWithEscapedCharacter() + public function testDatabaseNameWithEscapedCharacter(): void { $this->adapter->dropDatabase($this->config['database'] . '-test'); $this->adapter->createDatabase($this->config['database'] . '-test', ['charset' => 'utf8mb4']); @@ -138,24 +134,24 @@ public function testDatabaseNameWithEscapedCharacter() $this->adapter->dropDatabase($this->config['database'] . '-test'); } - public function testQuoteTableName() + public function testQuoteTableName(): void { $this->assertEquals('`test_table`', $this->adapter->quoteTableName('test_table')); } - public function testQuoteColumnName() + public function testQuoteColumnName(): void { $this->assertEquals('`test_column`', $this->adapter->quoteColumnName('test_column')); } - public function testHasTableUnderstandsSchemaNotation() + public function testHasTableUnderstandsSchemaNotation(): void { $this->assertTrue($this->adapter->hasTable('performance_schema.threads'), 'Failed asserting hasTable understands tables in another schema.'); $this->assertFalse($this->adapter->hasTable('performance_schema.unknown_table')); $this->assertFalse($this->adapter->hasTable('unknown_schema.phinxlog')); } - public function testCreateTable() + public function testCreateTable(): void { $table = new Table('ntable', [], $this->adapter); $table->addColumn('realname', 'string') @@ -173,7 +169,7 @@ public function testCreateTable() $this->assertFalse($columns[0]->isSigned()); } - public function testCreateTableWithComment() + public function testCreateTableWithComment(): void { $tableComment = 'Table comment'; $table = new Table('ntable', ['comment' => $tableComment], $this->adapter); @@ -193,7 +189,7 @@ public function testCreateTableWithComment() $this->assertEquals($tableComment, $comment['TABLE_COMMENT'], 'Dont set table comment correctly'); } - public function testCreateTableWithForeignKeys() + public function testCreateTableWithForeignKeys(): void { $tag_table = new Table('ntable_tag', [], $this->adapter); $tag_table->addColumn('realname', 'string') @@ -224,7 +220,7 @@ public function testCreateTableWithForeignKeys() $this->assertEquals($foreignKey['REFERENCED_COLUMN_NAME'], 'id'); } - public function testCreateTableCustomIdColumn() + public function testCreateTableCustomIdColumn(): void { $table = new Table('ntable', ['id' => 'custom_id'], $this->adapter); $table->addColumn('realname', 'string') @@ -237,7 +233,7 @@ public function testCreateTableCustomIdColumn() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableWithNoPrimaryKey() + public function testCreateTableWithNoPrimaryKey(): void { $options = [ 'id' => false, @@ -248,7 +244,7 @@ public function testCreateTableWithNoPrimaryKey() $this->assertFalse($this->adapter->hasColumn('atable', 'id')); } - public function testCreateTableWithConflictingPrimaryKeys() + public function testCreateTableWithConflictingPrimaryKeys(): void { $options = [ 'primary_key' => 'user_id', @@ -260,7 +256,7 @@ public function testCreateTableWithConflictingPrimaryKeys() $table->addColumn('user_id', 'integer')->save(); } - public function testCreateTableWithPrimaryKeySetToImplicitId() + public function testCreateTableWithPrimaryKeySetToImplicitId(): void { $options = [ 'primary_key' => 'id', @@ -272,7 +268,7 @@ public function testCreateTableWithPrimaryKeySetToImplicitId() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithPrimaryKeyArraySetToImplicitId() + public function testCreateTableWithPrimaryKeyArraySetToImplicitId(): void { $options = [ 'primary_key' => ['id'], @@ -284,7 +280,7 @@ public function testCreateTableWithPrimaryKeyArraySetToImplicitId() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId() + public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId(): void { $options = [ 'primary_key' => ['id', 'user_id'], @@ -295,7 +291,7 @@ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId() $table->addColumn('user_id', 'integer')->save(); } - public function testCreateTableWithMultiplePrimaryKeys() + public function testCreateTableWithMultiplePrimaryKeys(): void { $options = [ 'id' => false, @@ -314,7 +310,7 @@ public function testCreateTableWithMultiplePrimaryKeys() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsUuid() + public function testCreateTableWithPrimaryKeyAsUuid(): void { $options = [ 'id' => false, @@ -331,7 +327,7 @@ public function testCreateTableWithPrimaryKeyAsUuid() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsBinaryUuid() + public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void { $options = [ 'id' => false, @@ -345,7 +341,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableBinaryLengthWithIndex() + public function testCreateTableBinaryLengthWithIndex(): void { $table = new Table('ntable', [], $this->adapter); $table @@ -369,7 +365,7 @@ public function testCreateTableBinaryLengthWithIndex() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsNativeUuid() + public function testCreateTableWithPrimaryKeyAsNativeUuid(): void { if (!$this->usingMariaDbWithUuid()) { $this->markTestSkipped('Database does not have a native uuid type'); @@ -387,7 +383,7 @@ public function testCreateTableWithPrimaryKeyAsNativeUuid() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithMultipleIndexes() + public function testCreateTableWithMultipleIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -401,7 +397,7 @@ public function testCreateTableWithMultipleIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name'])); } - public function testCreateTableWithUniqueIndexes() + public function testCreateTableWithUniqueIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 191]) @@ -411,7 +407,7 @@ public function testCreateTableWithUniqueIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email'])); } - public function testCreateTableWithFullTextIndex() + public function testCreateTableWithFullTextIndex(): void { $table = new Table('table1', ['engine' => 'MyISAM'], $this->adapter); $table->addColumn('email', 'string') @@ -421,7 +417,7 @@ public function testCreateTableWithFullTextIndex() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email'])); } - public function testCreateTableWithNamedIndex() + public function testCreateTableWithNamedIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -432,7 +428,7 @@ public function testCreateTableWithNamedIndex() $this->assertTrue($this->adapter->hasIndexByName('table1', 'myemailindex')); } - public function testCreateTableWithMyISAMEngine() + public function testCreateTableWithMyISAMEngine(): void { $table = new Table('ntable', ['engine' => 'MyISAM'], $this->adapter); $table->addColumn('realname', 'string') @@ -442,7 +438,7 @@ public function testCreateTableWithMyISAMEngine() $this->assertEquals('MyISAM', $row['Engine']); } - public function testCreateTableAndInheritDefaultCollation() + public function testCreateTableAndInheritDefaultCollation(): void { $options = $this->config + [ 'charset' => 'utf8mb4', @@ -458,7 +454,7 @@ public function testCreateTableAndInheritDefaultCollation() $this->assertContains($row['Collation'], ['utf8mb4_general_ci', 'utf8mb4_0900_ai_ci', 'utf8mb4_uca1400_ai_ci', 'utf8mb4_unicode_ci']); } - public function testCreateTableWithLatin1Collate() + public function testCreateTableWithLatin1Collate(): void { $table = new Table('latin1_table', ['collation' => 'latin1_general_ci'], $this->adapter); $table->addColumn('name', 'string') @@ -468,7 +464,7 @@ public function testCreateTableWithLatin1Collate() $this->assertEquals('latin1_general_ci', $row['Collation']); } - public function testCreateTableWithSignedPK() + public function testCreateTableWithSignedPK(): void { $table = new Table('ntable', ['signed' => true], $this->adapter); $table->addColumn('realname', 'string') @@ -487,7 +483,7 @@ public function testCreateTableWithSignedPK() } } - public function testCreateTableWithUnsignedPK() + public function testCreateTableWithUnsignedPK(): void { $table = new Table('ntable', ['signed' => false], $this->adapter); $table->addColumn('realname', 'string') @@ -506,7 +502,7 @@ public function testCreateTableWithUnsignedPK() } } - public function testCreateTableWithUnsignedNamedPK() + public function testCreateTableWithUnsignedNamedPK(): void { $table = new Table('ntable', ['id' => 'named_id', 'signed' => false], $this->adapter); $table->addColumn('realname', 'string') @@ -525,7 +521,7 @@ public function testCreateTableWithUnsignedNamedPK() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableWithSetEnumTypes() + public function testCreateTableWithSetEnumTypes(): void { $table = new Table('enum_test', [], $this->adapter); $table->addColumn('status', 'enum', ['values' => ['pending', 'active', 'archived']]) @@ -538,7 +534,7 @@ public function testCreateTableWithSetEnumTypes() } #[RunInSeparateProcess] - public function testUnsignedPksFeatureFlag() + public function testUnsignedPksFeatureFlag(): void { $this->adapter->connect(); @@ -554,7 +550,7 @@ public function testUnsignedPksFeatureFlag() } #[RunInSeparateProcess] - public function testAddTimestampsFeatureFlag() + public function testAddTimestampsFeatureFlag(): void { Configure::write('Migrations.add_timestamps_use_datetime', true); $this->adapter->connect(); @@ -581,7 +577,7 @@ public function testAddTimestampsFeatureFlag() $this->assertNull($columns[2]->getDefault()); } - public function testCreateTableWithSchema() + public function testCreateTableWithSchema(): void { $table = new Table($this->config['database'] . '.ntable', [], $this->adapter); $table->addColumn('realname', 'string') @@ -590,7 +586,7 @@ public function testCreateTableWithSchema() $this->assertTrue($this->adapter->hasTable('ntable')); } - public function testAddPrimarykey() + public function testAddPrimarykey(): void { $table = new Table('table1', ['id' => false], $this->adapter); $table @@ -604,7 +600,7 @@ public function testAddPrimarykey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testChangePrimaryKey() + public function testChangePrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -621,7 +617,7 @@ public function testChangePrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2', 'column3'])); } - public function testDropPrimaryKey() + public function testDropPrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -635,7 +631,7 @@ public function testDropPrimaryKey() $this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testAddComment() + public function testAddComment(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -657,7 +653,7 @@ public function testAddComment() $this->assertEquals('comment1', $rows[0]['TABLE_COMMENT']); } - public function testChangeComment() + public function testChangeComment(): void { $table = new Table('table1', ['comment' => 'comment1'], $this->adapter); $table->save(); @@ -679,7 +675,7 @@ public function testChangeComment() $this->assertEquals('comment2', $rows[0]['TABLE_COMMENT']); } - public function testDropComment() + public function testDropComment(): void { $table = new Table('table1', ['comment' => 'comment1'], $this->adapter); $table->save(); @@ -701,7 +697,7 @@ public function testDropComment() $this->assertEquals('', $rows[0]['TABLE_COMMENT']); } - public function testRenameTable() + public function testRenameTable(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -713,7 +709,7 @@ public function testRenameTable() $this->assertTrue($this->adapter->hasTable('table2')); } - public function testAddColumn() + public function testAddColumn(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -727,7 +723,7 @@ public function testAddColumn() $this->assertEquals('realname', $rows[1]['Field']); } - public function testAddColumnWithDefaultValue() + public function testAddColumnWithDefaultValue(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -737,7 +733,7 @@ public function testAddColumnWithDefaultValue() $this->assertEquals('test', $rows[1]['Default']); } - public function testAddColumnWithDefaultZero() + public function testAddColumnWithDefaultZero(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -748,7 +744,7 @@ public function testAddColumnWithDefaultZero() $this->assertEquals('0', $rows[1]['Default']); } - public function testAddColumnWithDefaultEmptyString() + public function testAddColumnWithDefaultEmptyString(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -758,7 +754,7 @@ public function testAddColumnWithDefaultEmptyString() $this->assertEquals('', $rows[1]['Default']); } - public function testAddColumnWithDefaultBoolean() + public function testAddColumnWithDefaultBoolean(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -772,7 +768,7 @@ public function testAddColumnWithDefaultBoolean() $this->assertNull($rows[3]['Default']); } - public function testAddColumnWithDefaultLiteral() + public function testAddColumnWithDefaultLiteral(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -786,7 +782,7 @@ public function testAddColumnWithDefaultLiteral() $this->assertTrue($rows[2]['Default'] === 'oh hi'); } - public function testAddColumnFirst() + public function testAddColumnFirst(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -796,7 +792,7 @@ public function testAddColumnFirst() $this->assertSame('new_id', $rows[0]['Field']); } - public static function integerDataProvider() + public static function integerDataProvider(): array { return [ ['integer', [], 'int', '11', ''], @@ -811,7 +807,7 @@ public static function integerDataProvider() } #[DataProvider('integerDataProvider')] - public function testIntegerColumnTypes($phinx_type, $options, $sql_type, $width, $extra) + public function testIntegerColumnTypes(string $phinx_type, array $options, string $sql_type, string $width, string $extra): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -897,7 +893,7 @@ public function testRenameColumnWithDefaultGeneratedExtra(): void $this->assertTrue($this->adapter->hasColumn('t', 'last_changed2')); } - public function testRenamingANonExistentColumn() + public function testRenamingANonExistentColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -910,13 +906,13 @@ public function testRenamingANonExistentColumn() $this->assertInstanceOf( 'InvalidArgumentException', $e, - 'Expected exception of type InvalidArgumentException, got ' . get_class($e), + 'Expected exception of type InvalidArgumentException, got ' . $e::class, ); - $this->assertEquals('The specified column doesn\'t exist: column2', $e->getMessage()); + $this->assertEquals("The specified column doesn't exist: column2", $e->getMessage()); } } - public function testChangeColumn() + public function testChangeColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -933,7 +929,7 @@ public function testChangeColumn() $this->assertTrue($this->adapter->hasColumn('t', 'column2')); } - public function testChangeColumnDefaultValue() + public function testChangeColumnDefaultValue(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test']) @@ -948,7 +944,7 @@ public function testChangeColumnDefaultValue() $this->assertEquals('test1', $rows[1]['Default']); } - public function testChangeColumnDefaultToZero() + public function testChangeColumnDefaultToZero(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'integer') @@ -963,7 +959,7 @@ public function testChangeColumnDefaultToZero() $this->assertEquals('0', $rows[1]['Default']); } - public function testChangeColumnDefaultToNull() + public function testChangeColumnDefaultToNull(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test']) @@ -977,7 +973,7 @@ public function testChangeColumnDefaultToNull() $this->assertNull($rows[1]['Default']); } - public function testChangeColumnPreservesDefaultValue() + public function testChangeColumnPreservesDefaultValue(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'original_default', 'null' => false, 'limit' => 100]) @@ -992,7 +988,7 @@ public function testChangeColumnPreservesDefaultValue() $this->assertEquals('varchar(100)', $rows[1]['Type']); } - public function testChangeColumnPreservesDefaultValueWithDifferentType() + public function testChangeColumnPreservesDefaultValueWithDifferentType(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'integer', ['default' => 42, 'null' => false]) @@ -1006,7 +1002,7 @@ public function testChangeColumnPreservesDefaultValueWithDifferentType() $this->assertEquals('NO', $rows[1]['Null']); } - public function testChangeColumnCanExplicitlyOverrideDefault() + public function testChangeColumnCanExplicitlyOverrideDefault(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'original_default']) @@ -1019,7 +1015,7 @@ public function testChangeColumnCanExplicitlyOverrideDefault() $this->assertEquals('new_default', $rows[1]['Default']); } - public function testChangeColumnCanDisablePreserveUnspecified() + public function testChangeColumnCanDisablePreserveUnspecified(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'original_default', 'limit' => 100]) @@ -1032,7 +1028,7 @@ public function testChangeColumnCanDisablePreserveUnspecified() $this->assertNull($rows[1]['Default']); } - public function testChangeColumnWithNullTypePreservesType() + public function testChangeColumnWithNullTypePreservesType(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) @@ -1047,7 +1043,7 @@ public function testChangeColumnWithNullTypePreservesType() $this->assertEquals('YES', $rows[1]['Null']); } - public function testChangeColumnWithNullTypeOnNonExistentColumnThrows() + public function testChangeColumnWithNullTypeOnNonExistentColumnThrows(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage("Cannot preserve column type for 'nonexistent'"); @@ -1059,7 +1055,7 @@ public function testChangeColumnWithNullTypeOnNonExistentColumnThrows() $table->changeColumn('nonexistent', null, ['null' => true])->save(); } - public function testUpdateColumnPreservesAttributes() + public function testUpdateColumnPreservesAttributes(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100, 'null' => false]) @@ -1074,7 +1070,7 @@ public function testUpdateColumnPreservesAttributes() $this->assertEquals('YES', $rows[1]['Null']); } - public function testChangeColumnDoesNotPreserveByDefault() + public function testChangeColumnDoesNotPreserveByDefault(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) @@ -1089,7 +1085,7 @@ public function testChangeColumnDoesNotPreserveByDefault() $this->assertEquals('YES', $rows[1]['Null']); } - public function testChangeColumnWithPreserveUnspecifiedTrue() + public function testChangeColumnWithPreserveUnspecifiedTrue(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100]) @@ -1104,7 +1100,7 @@ public function testChangeColumnWithPreserveUnspecifiedTrue() $this->assertEquals('YES', $rows[1]['Null']); } - public function testUpdateColumnWithColumnObject() + public function testUpdateColumnWithColumnObject(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test', 'limit' => 100, 'null' => false]) @@ -1123,7 +1119,7 @@ public function testUpdateColumnWithColumnObject() $this->assertEquals('YES', $rows[1]['Null']); } - public function testUpdateColumnWithColumnObjectAndOptionsThrows() + public function testUpdateColumnWithColumnObjectAndOptionsThrows(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot specify options array when passing a Column object'); @@ -1141,7 +1137,7 @@ public function testUpdateColumnWithColumnObjectAndOptionsThrows() $table->updateColumn('column1', $newColumn, ['limit' => 500]); } - public function testUpdateColumnWithTypeChangeToText() + public function testUpdateColumnWithTypeChangeToText(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test']) @@ -1161,7 +1157,7 @@ public function testUpdateColumnWithTypeChangeToText() $this->assertStringContainsString('test', $rows[1]['Default']); // Default should be preserved } - public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType() + public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['limit' => 100, 'default' => 'test']) @@ -1181,7 +1177,7 @@ public function testUpdateColumnCanRemoveLengthConstraintWithoutChangingType() $this->assertEquals('test', $rows[1]['Default']); // Default should be preserved } - public function testUpdateColumnCanRemoveScaleAndPrecision() + public function testUpdateColumnCanRemoveScaleAndPrecision(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'decimal', ['precision' => 10, 'scale' => 2, 'default' => '123.45']) @@ -1200,7 +1196,7 @@ public function testUpdateColumnCanRemoveScaleAndPrecision() $this->assertEquals('123', $rows[1]['Default']); // Default should be preserved (truncated to integer) } - public function testUpdateColumnCanRemoveComment() + public function testUpdateColumnCanRemoveComment(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['limit' => 100, 'comment' => 'Original comment', 'default' => 'test']) @@ -1220,7 +1216,7 @@ public function testUpdateColumnCanRemoveComment() $this->assertEquals('test', $rows[1]['Default']); } - public function testChangeColumnEnum() + public function testChangeColumnEnum(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1235,7 +1231,7 @@ public function testChangeColumnEnum() $this->assertEquals("enum('a','b')", $rows[1]['Type']); } - public static function binaryToBlobAutomaticConversionData() + public static function binaryToBlobAutomaticConversionData(): array { return [ // When creating binary with limit > 255, MySQL auto-converts to BLOB @@ -1255,7 +1251,7 @@ public static function binaryToBlobAutomaticConversionData() } #[DataProvider('binaryToBlobAutomaticConversionData')] - public function testBinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit) + public function testBinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'binary', ['limit' => $limit]) @@ -1265,7 +1261,7 @@ public function testBinaryToBlobAutomaticConversion(?int $limit, string $expecte $this->assertSame($expectedLimit, $columns[1]->getLimit()); } - public static function varbinaryToBlobAutomaticConversionData() + public static function varbinaryToBlobAutomaticConversionData(): array { return [ // When creating varbinary with limit > 255, MySQL auto-converts to BLOB @@ -1285,7 +1281,7 @@ public static function varbinaryToBlobAutomaticConversionData() } #[DataProvider('varbinaryToBlobAutomaticConversionData')] - public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit) + public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expectedType, ?int $expectedLimit): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'varbinary', ['limit' => $limit]) @@ -1295,7 +1291,7 @@ public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expe $this->assertSame($expectedLimit, $columns[1]->getLimit()); } - public static function blobColumnsData() + public static function blobColumnsData(): array { return [ // BLOB columns with various limits - MySQL auto-selects appropriate BLOB subtype @@ -1328,7 +1324,7 @@ public static function blobColumnsData() } #[DataProvider('blobColumnsData')] - public function testblobColumns(string $type, string $expectedType, ?int $limit, ?int $expectedLimit) + public function testblobColumns(string $type, string $expectedType, ?int $limit, ?int $expectedLimit): void { $table = new Table('t', [], $this->adapter); $table->addColumn('blob_col', $type, ['limit' => $limit]) @@ -1338,7 +1334,7 @@ public function testblobColumns(string $type, string $expectedType, ?int $limit, $this->assertSame($expectedLimit, $columns[1]->getLimit()); } - public static function blobRoundTripData() + public static function blobRoundTripData(): array { return [ // type, limit, expected type after round-trip, expected limit after round-trip @@ -1351,7 +1347,7 @@ public static function blobRoundTripData() } #[DataProvider('blobRoundTripData')] - public function testBlobRoundTrip(string $type, ?int $limit, string $expectedType, int $expectedLimit) + public function testBlobRoundTrip(string $type, ?int $limit, string $expectedType, int $expectedLimit): void { // Create a table with a BLOB column $table = new Table('blob_round_trip_test', [], $this->adapter); @@ -1370,7 +1366,7 @@ public function testBlobRoundTrip(string $type, ?int $limit, string $expectedTyp $this->adapter->dropTable('blob_round_trip_test'); } - public static function textRoundTripData() + public static function textRoundTripData(): array { return [ // type, limit, expected type after round-trip, expected limit after round-trip @@ -1385,7 +1381,7 @@ public static function textRoundTripData() } #[DataProvider('textRoundTripData')] - public function testTextRoundTrip(string $type, ?int $limit, string $expectedType, ?int $expectedLimit) + public function testTextRoundTrip(string $type, ?int $limit, string $expectedType, ?int $expectedLimit): void { // Create a table with a TEXT column $table = new Table('text_round_trip_test', [], $this->adapter); @@ -1404,7 +1400,7 @@ public function testTextRoundTrip(string $type, ?int $limit, string $expectedTyp $this->adapter->dropTable('text_round_trip_test'); } - public function testTimestampInvalidLimit() + public function testTimestampInvalidLimit(): void { $this->adapter->connect(); $version = $this->adapter->getConnection()->getDriver()->version(); @@ -1418,7 +1414,7 @@ public function testTimestampInvalidLimit() $table->addColumn('column1', 'timestamp', ['limit' => 7])->save(); } - public function testDropColumn() + public function testDropColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1429,7 +1425,7 @@ public function testDropColumn() $this->assertFalse($this->adapter->hasColumn('t', 'column1')); } - public static function columnsProvider() + public static function columnsProvider(): array { return [ ['column1', 'string', []], @@ -1459,7 +1455,7 @@ public static function columnsProvider() } #[DataProvider('columnsProvider')] - public function testGetColumns($colName, $type, $options) + public function testGetColumns(string $colName, string $type, array $options): void { $table = new Table('t', [], $this->adapter); $table->addColumn($colName, $type, $options)->save(); @@ -1490,7 +1486,7 @@ public function testGetColumns($colName, $type, $options) } } - public function testGetColumnsInteger() + public function testGetColumnsInteger(): void { $colName = 'column15'; $type = 'integer'; @@ -1506,7 +1502,7 @@ public function testGetColumnsInteger() $this->assertNull($columns[1]->getLimit()); } - public function testGetColumnsReservedTableName() + public function testGetColumnsReservedTableName(): void { $table = new Table('group', [], $this->adapter); $table->addColumn('column1', 'string')->save(); @@ -1514,7 +1510,7 @@ public function testGetColumnsReservedTableName() $this->assertCount(2, $columns); } - public function testAddIndex() + public function testAddIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1525,7 +1521,7 @@ public function testAddIndex() $this->assertTrue($table->hasIndex('email')); } - public function testAddIndexWithSort() + public function testAddIndexWithSort(): void { $this->adapter->connect(); if (!$this->usingMysql8()) { @@ -1548,7 +1544,7 @@ public function testAddIndexWithSort() $this->assertEquals($emailOrder, 'A'); } - public function testAddMultipleFulltextIndex() + public function testAddMultipleFulltextIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1569,7 +1565,7 @@ public function testAddMultipleFulltextIndex() $this->assertTrue($table->hasIndex(['email', 'bio'])); } - public function testAddIndexWithLimit() + public function testAddIndexWithLimit(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1586,7 +1582,7 @@ public function testAddIndexWithLimit() $this->assertEquals($expected_limit, 50); } - public function testAddMultiIndexesWithLimitSpecifier() + public function testAddMultiIndexesWithLimitSpecifier(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1610,7 +1606,7 @@ public function testAddMultiIndexesWithLimitSpecifier() $this->assertEquals($expected_limit, 2); } - public function testAddSingleIndexesWithLimitSpecifier() + public function testAddSingleIndexesWithLimitSpecifier(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1628,7 +1624,7 @@ public function testAddSingleIndexesWithLimitSpecifier() $this->assertEquals($expected_limit, 3); } - public function testDropIndex() + public function testDropIndex(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -1678,7 +1674,7 @@ public function testDropIndex() try { $table2->removeIndex(['fname'])->save(); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { } $this->assertTrue($table2->hasIndex(['fname', 'lname'])); @@ -1693,13 +1689,13 @@ public function testDropIndex() try { $table4->removeIndex(['fname'])->save(); - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { } $this->assertTrue($table4->hasIndex(['fname', 'lname'])); } - public function testDropIndexByName() + public function testDropIndexByName(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -1721,7 +1717,7 @@ public function testDropIndexByName() $this->assertFalse($table2->hasIndex(['fname', 'lname'])); } - public function testAddForeignKey() + public function testAddForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1735,7 +1731,7 @@ public function testAddForeignKey() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testAddForeignKeyForTableWithSignedPK() + public function testAddForeignKeyForTableWithSignedPK(): void { $refTable = new Table('ref_table', ['signed' => true], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1749,7 +1745,7 @@ public function testAddForeignKeyForTableWithSignedPK() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKey() + public function testDropForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1764,7 +1760,7 @@ public function testDropForeignKey() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyWithMultipleColumns() + public function testDropForeignKeyWithMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1814,7 +1810,7 @@ public function testDropForeignKeyWithMultipleColumns() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_field1', 'ref_table_id'])); } - public function testDropForeignKeyWithIdenticalMultipleColumns() + public function testDropForeignKeyWithIdenticalMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1862,11 +1858,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array ]; } - /** - * @param array $columns - */ #[DataProvider('nonExistentForeignKeyColumnsProvider')] - public function testDropForeignKeyByNonExistentKeyColumns(array $columns) + public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1894,7 +1887,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns) $this->adapter->dropForeignKey($table->getName(), $columns); } - public function testDropForeignKeyCaseInsensitivity() + public function testDropForeignKeyCaseInsensitivity(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1909,7 +1902,7 @@ public function testDropForeignKeyCaseInsensitivity() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyByName() + public function testDropForeignKeyByName(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1929,7 +1922,7 @@ public function testDropForeignKeyByName() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyForTableWithSignedPK() + public function testDropForeignKeyForTableWithSignedPK(): void { $refTable = new Table('ref_table', ['signed' => true], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1944,7 +1937,7 @@ public function testDropForeignKeyForTableWithSignedPK() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyAsString() + public function testDropForeignKeyAsString(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1960,7 +1953,7 @@ public function testDropForeignKeyAsString() } #[DataProvider('provideForeignKeysToCheck')] - public function testHasForeignKey($tableDef, $key, $exp) + public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void { $conn = $this->adapter->getConnection(); $conn->execute('CREATE TABLE other(a int, b int, c int, key(a), key(b), key(a,b), key(a,b,c));'); @@ -1968,7 +1961,7 @@ public function testHasForeignKey($tableDef, $key, $exp) $this->assertSame($exp, $this->adapter->hasForeignKey('t', $key)); } - public static function provideForeignKeysToCheck() + public static function provideForeignKeysToCheck(): array { return [ ['create table t(a int)', 'a', false], @@ -1994,7 +1987,7 @@ public static function provideForeignKeysToCheck() ]; } - public function testHasForeignKeyAsString() + public function testHasForeignKeyAsString(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -2009,7 +2002,7 @@ public function testHasForeignKeyAsString() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), 'ref_table_id2')); } - public function testHasNamedForeignKey() + public function testHasNamedForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -2032,7 +2025,7 @@ public function testHasNamedForeignKey() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'my_constraint2')); } - public function testHasForeignKeyWithConstraintForTableWithSignedPK() + public function testHasForeignKeyWithConstraintForTableWithSignedPK(): void { $refTable = new Table('ref_table', ['signed' => true], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -2052,7 +2045,7 @@ public function testHasForeignKeyWithConstraintForTableWithSignedPK() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'], 'my_constraint2')); } - public function testsHasForeignKeyWithSchemaDotTableName() + public function testsHasForeignKeyWithSchemaDotTableName(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -2067,13 +2060,13 @@ public function testsHasForeignKeyWithSchemaDotTableName() $this->assertFalse($this->adapter->hasForeignKey($this->config['database'] . '.' . $table->getName(), ['ref_table_id2'])); } - public function testHasDatabase() + public function testHasDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('fake_database_name')); $this->assertTrue($this->adapter->hasDatabase($this->config['database'])); } - public function testDropDatabase() + public function testDropDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('phinx_temp_database')); $this->adapter->createDatabase('phinx_temp_database'); @@ -2081,7 +2074,7 @@ public function testDropDatabase() $this->adapter->dropDatabase('phinx_temp_database'); } - public function testAddColumnWithComment() + public function testAddColumnWithComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string', ['comment' => $comment = 'Comments from "column1"']) @@ -2100,7 +2093,7 @@ public function testAddColumnWithComment() $this->assertEquals($comment, $columnWithComment['COLUMN_COMMENT'], "Didn't set column comment correctly"); } - public function testAddColumnEnum() + public function testAddColumnEnum(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -2134,7 +2127,7 @@ public function testAddColumnEnum() $this->assertEquals($comment, $columnWithComment['COLUMN_COMMENT'], "Didn't set column comment correctly"); } - public function testAddGeoSpatialColumns() + public function testAddGeoSpatialColumns(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -2145,7 +2138,7 @@ public function testAddGeoSpatialColumns() $this->assertEquals('geometry', $rows[1]['Type']); } - public function testHasColumn() + public function testHasColumn(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -2155,7 +2148,7 @@ public function testHasColumn() $this->assertTrue($table->hasColumn('column1')); } - public function testHasColumnReservedName() + public function testHasColumnReservedName(): void { $tableQuoted = new Table('group', [], $this->adapter); $tableQuoted->addColumn('value', 'string') @@ -2165,7 +2158,7 @@ public function testHasColumnReservedName() $this->assertTrue($tableQuoted->hasColumn('value')); } - public function testBulkInsertData() + public function testBulkInsertData(): void { $data = [ [ @@ -2199,7 +2192,7 @@ public function testBulkInsertData() $this->assertEquals('test', $rows[2]['column3']); } - public function testBulkInsertLiteral() + public function testBulkInsertLiteral(): void { $data = [ [ @@ -2225,12 +2218,12 @@ public function testBulkInsertLiteral() $this->assertEquals('value1', $rows[0]['column1']); $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']); $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); } - public function testInsertData() + public function testInsertData(): void { $data = [ [ @@ -2265,7 +2258,7 @@ public function testInsertData() $this->assertEquals('foo', $rows[2]['column3']); } - public function testInsertLiteral() + public function testInsertLiteral(): void { $data = [ [ @@ -2296,12 +2289,12 @@ public function testInsertLiteral() $this->assertEquals('test', $rows[0]['column2']); $this->assertEquals('test', $rows[1]['column2']); $this->assertEquals('foo', $rows[2]['column2']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']); $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); } - public function testDumpCreateTable() + public function testDumpCreateTable(): void { $options = $this->adapter->getOptions(); $options['dryrun'] = true; @@ -2314,7 +2307,7 @@ public function testDumpCreateTable() ->addColumn('column3', 'string', ['default' => 'test', 'null' => false]) ->save(); - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); // MySQL version affects default collation (8.0.0+ uses utf8mb4_0900_ai_ci, older uses utf8mb4_general_ci) // MariaDB 11.8 uses: utf8mb4_uca1400_ai_ci $this->assertMatchesRegularExpression( @@ -2329,7 +2322,7 @@ public function testDumpCreateTable() * Then enables dry run mode and inserts a record. * Asserts that the insert statement is output and doesn't insert a record. */ - public function testDumpInsert() + public function testDumpInsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2354,13 +2347,13 @@ public function testDumpInsert() INSERT INTO `table1` (`string_col`) VALUES (null); INSERT INTO `table1` (`int_col`) VALUES (23); OUTPUT; - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); // Add this to be LF - CR/LF systems independent $expectedOutput = preg_replace('~\R~u', '', $expectedOutput); $actualOutput = preg_replace('~\R~u', '', $actualOutput); - $this->assertStringContainsString($expectedOutput, trim($actualOutput), 'Passing the --dry-run option doesn\'t dump the insert to the output'); + $this->assertStringContainsString($expectedOutput, trim((string)$actualOutput), "Passing the --dry-run option doesn't dump the insert to the output"); $countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1'); $this->assertTrue($countQuery->execute()); @@ -2373,7 +2366,7 @@ public function testDumpInsert() * Then enables dry run mode and inserts some records. * Asserts that output contains the insert statement and doesn't insert any record. */ - public function testDumpBulkinsert() + public function testDumpBulkinsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2395,8 +2388,8 @@ public function testDumpBulkinsert() $expectedOutput = <<<'OUTPUT' INSERT INTO `table1` (`string_col`, `int_col`) VALUES ('test_data1', 23), (null, 42); OUTPUT; - $actualOutput = join("\n", $this->out->messages()); - $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option doesn\'t dump the bulkinsert to the output'); + $actualOutput = implode("\n", $this->out->messages()); + $this->assertStringContainsString($expectedOutput, $actualOutput, "Passing the --dry-run option doesn't dump the bulkinsert to the output"); $countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1'); $this->assertTrue($countQuery->execute()); @@ -2404,7 +2397,7 @@ public function testDumpBulkinsert() $this->assertEquals(0, $res[0]['COUNT(*)']); } - public function testDumpCreateTableAndThenInsert() + public function testDumpCreateTableAndThenInsert(): void { $options = $this->adapter->getOptions(); $options['dryrun'] = true; @@ -2422,7 +2415,7 @@ public function testDumpCreateTableAndThenInsert() 'column2' => 1, ])->save(); - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); // Add this to be LF - CR/LF systems independent $actualOutput = preg_replace('~\R~u', '', $actualOutput); // MySQL version affects default collation (8.0.0+ uses utf8mb4_0900_ai_ci, older uses utf8mb4_general_ci) @@ -2436,7 +2429,7 @@ public function testDumpCreateTableAndThenInsert() /** * Tests interaction with the query builder */ - public function testQueryBuilder() + public function testQueryBuilder(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2475,7 +2468,7 @@ public function testQueryBuilder() $this->assertEquals(1, $stm->rowCount()); } - public function testQueryWithParams() + public function testQueryWithParams(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2506,7 +2499,7 @@ public function testQueryWithParams() $this->assertEquals(3, $res[0]['c']); } - public static function geometryTypeProvider() + public static function geometryTypeProvider(): array { return [ [MysqlAdapter::TYPE_GEOMETRY, 'POINT(0 0)'], @@ -2516,12 +2509,8 @@ public static function geometryTypeProvider() ]; } - /** - * @param string $type - * @param string $geom - */ #[DataProvider('geometryTypeProvider')] - public function testGeometrySridSupport($type, $geom) + public function testGeometrySridSupport(string $type, string $geom): void { $this->adapter->connect(); if (!$this->usingMysql8()) { @@ -2533,19 +2522,15 @@ public function testGeometrySridSupport($type, $geom) ->addColumn('geom', $type, ['srid' => 4326]) ->save(); - $this->adapter->execute("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('{$geom}', 4326))"); + $this->adapter->execute(sprintf("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('%s', 4326))", $geom)); $rows = $this->adapter->fetchAll('SELECT ST_AsWKT(geom) as wkt, ST_SRID(geom) as srid FROM table1'); $this->assertCount(1, $rows); $this->assertSame($geom, $rows[0]['wkt']); $this->assertSame(4326, (int)$rows[0]['srid']); } - /** - * @param string $type - * @param string $geom - */ #[DataProvider('geometryTypeProvider')] - public function testGeometrySridThrowsInsertDifferentSrid($type, $geom) + public function testGeometrySridThrowsInsertDifferentSrid(string $type, string $geom): void { $this->adapter->connect(); if (!$this->usingMysql8()) { @@ -2559,10 +2544,10 @@ public function testGeometrySridThrowsInsertDifferentSrid($type, $geom) $this->expectException(PDOException::class); $this->expectExceptionMessage("SQLSTATE[HY000]: General error: 3643 The SRID of the geometry does not match the SRID of the column 'geom'. The SRID of the geometry is 4322, but the SRID of the column is 4326. Consider changing the SRID of the geometry or the SRID property of the column."); - $this->adapter->execute("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('{$geom}', 4322))"); + $this->adapter->execute(sprintf("INSERT INTO table1 (`geom`) VALUES (ST_GeomFromText('%s', 4322))", $geom)); } - public static function defaultsCastAsExpressions() + public static function defaultsCastAsExpressions(): array { return [ [MysqlAdapter::TYPE_JSON, '{"a": true}'], @@ -2577,20 +2562,21 @@ public static function defaultsCastAsExpressions() /** * MySQL 8 added support for specifying defaults for the BLOB, TEXT, GEOMETRY, and JSON data types, * however requiring that they be wrapped in expressions. - * - * @param string $type - * @param string $default */ #[DataProvider('defaultsCastAsExpressions')] public function testDefaultsCastAsExpressionsForCertainTypes(string $type, string $default): void { if ( - $this->usingMariaDb() && in_array($type, [ - MysqlAdapter::TYPE_GEOMETRY, - MysqlAdapter::TYPE_POINT, - MysqlAdapter::TYPE_LINESTRING, - MysqlAdapter::TYPE_POLYGON, - ]) + $this->usingMariaDb() && in_array( + $type, + [ + MysqlAdapter::TYPE_GEOMETRY, + MysqlAdapter::TYPE_POINT, + MysqlAdapter::TYPE_LINESTRING, + MysqlAdapter::TYPE_POLYGON, + ], + true, + ) ) { $this->markTestSkipped('GIS is broken with MariaDB'); } @@ -2612,13 +2598,13 @@ public function testDefaultsCastAsExpressionsForCertainTypes(string $type, strin $actualDefault = $columns[0]->getDefault(); // Normalize quote handling - both MariaDB and MySQL 8.0.13+ may return defaults with quotes - if (str_starts_with($actualDefault, "'") && str_ends_with($actualDefault, "'")) { - $actualDefault = substr($actualDefault, 1, -1); + if (str_starts_with((string)$actualDefault, "'") && str_ends_with((string)$actualDefault, "'")) { + $actualDefault = substr((string)$actualDefault, 1, -1); } $this->assertSame($default, $actualDefault); } - public function testCreateTableWithPrecisionCurrentTimestamp() + public function testCreateTableWithPrecisionCurrentTimestamp(): void { $this->adapter->connect(); (new Table('exampleCurrentTimestamp3', ['id' => false], $this->adapter)) @@ -2637,7 +2623,7 @@ public function testCreateTableWithPrecisionCurrentTimestamp() $this->assertEqualsIgnoringCase('CURRENT_TIMESTAMP(3)', $colDef['COLUMN_DEFAULT']); } - public function testAddCheckConstraint() + public function testAddCheckConstraint(): void { $table = new Table('check_table', [], $this->adapter); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) @@ -2649,7 +2635,7 @@ public function testAddCheckConstraint() $this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive')); } - public function testAddCheckConstraintWithAutoGeneratedName() + public function testAddCheckConstraintWithAutoGeneratedName(): void { $table = new Table('check_table2', [], $this->adapter); $table->addColumn('age', 'integer') @@ -2669,7 +2655,7 @@ public function testAddCheckConstraintWithAutoGeneratedName() $this->assertStringContainsString($expected, $constraints[0]['name']); } - public function testHasCheckConstraint() + public function testHasCheckConstraint(): void { $table = new Table('check_table3', [], $this->adapter); $table->addColumn('quantity', 'integer') @@ -2683,7 +2669,7 @@ public function testHasCheckConstraint() $this->assertTrue($this->adapter->hasCheckConstraint('check_table3', 'quantity_positive')); } - public function testDropCheckConstraint() + public function testDropCheckConstraint(): void { $table = new Table('check_table4', [], $this->adapter); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) @@ -2697,7 +2683,7 @@ public function testDropCheckConstraint() $this->assertFalse($this->adapter->hasCheckConstraint('check_table4', 'price_check')); } - public function testCheckConstraintWithComplexExpression() + public function testCheckConstraintWithComplexExpression(): void { $table = new Table('check_table5', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -2714,7 +2700,7 @@ public function testCheckConstraintWithComplexExpression() // Verify the constraint is actually enforced $quotedTableName = $this->adapter->getConnection()->getDriver()->quoteIdentifier('check_table5'); $this->expectException(PDOException::class); - $this->adapter->execute("INSERT INTO {$quotedTableName} (email, status) VALUES ('test@example.com', 'invalid')"); + $this->adapter->execute(sprintf("INSERT INTO %s (email, status) VALUES ('test@example.com', 'invalid')", $quotedTableName)); } /** @@ -2727,7 +2713,7 @@ public function testCheckConstraintWithComplexExpression() * The 5.x branch uses CakePHP's database layer instead of phinx, * so we need to verify it handles scale=0 correctly. */ - public function testDecimalWithScaleZero() + public function testDecimalWithScaleZero(): void { // Create table with DECIMAL(65,0) $table = new Table('decimal_scale_zero_test', [], $this->adapter); @@ -2761,7 +2747,7 @@ public function testDecimalWithScaleZero() ); } - public function testInsertOrSkipWithDuplicates() + public function testInsertOrSkipWithDuplicates(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -2784,7 +2770,7 @@ public function testInsertOrSkipWithDuplicates() $this->assertEquals('John', $rows[0]['name']); } - public function testInsertModeResetsAfterInsertOrSkip() + public function testInsertModeResetsAfterInsertOrSkip(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -2804,7 +2790,7 @@ public function testInsertModeResetsAfterInsertOrSkip() ])->save(); } - public function testBulkinsertOrSkipWithDuplicates() + public function testBulkinsertOrSkipWithDuplicates(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('sku', 'string', ['limit' => 50]) @@ -2831,7 +2817,7 @@ public function testBulkinsertOrSkipWithDuplicates() $this->assertEquals('30.00', $rows[2]['price']); } - public function testInsertOrSkipWithoutDuplicates() + public function testInsertOrSkipWithoutDuplicates(): void { $table = new Table('categories', [], $this->adapter); $table->addColumn('name', 'string') @@ -2847,7 +2833,7 @@ public function testInsertOrSkipWithoutDuplicates() $this->assertCount(2, $rows); } - public function testAddColumnWithAlgorithmInstant() + public function testAddColumnWithAlgorithmInstant(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string') @@ -2861,7 +2847,7 @@ public function testAddColumnWithAlgorithmInstant() $this->assertTrue($this->adapter->hasColumn('users', 'status')); } - public function testAddColumnWithAlgorithmAndLock() + public function testAddColumnWithAlgorithmAndLock(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('name', 'string') @@ -2879,7 +2865,7 @@ public function testAddColumnWithAlgorithmAndLock() $this->assertTrue($this->adapter->hasColumn('products', 'price')); } - public function testChangeColumnWithAlgorithm() + public function testChangeColumnWithAlgorithm(): void { $table = new Table('items', [], $this->adapter); $table->addColumn('description', 'string', ['limit' => 100]) @@ -2899,7 +2885,7 @@ public function testChangeColumnWithAlgorithm() } } - public function testBatchedOperationsWithSameAlgorithm() + public function testBatchedOperationsWithSameAlgorithm(): void { $table = new Table('batch_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2919,7 +2905,7 @@ public function testBatchedOperationsWithSameAlgorithm() $this->assertTrue($this->adapter->hasColumn('batch_test', 'col3')); } - public function testBatchedOperationsWithConflictingAlgorithmsThrowsException() + public function testBatchedOperationsWithConflictingAlgorithmsThrowsException(): void { $table = new Table('conflict_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2939,7 +2925,7 @@ public function testBatchedOperationsWithConflictingAlgorithmsThrowsException() ->update(); } - public function testBatchedOperationsWithConflictingLocksThrowsException() + public function testBatchedOperationsWithConflictingLocksThrowsException(): void { $table = new Table('lock_conflict_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2961,7 +2947,7 @@ public function testBatchedOperationsWithConflictingLocksThrowsException() ->update(); } - public function testInvalidAlgorithmThrowsException() + public function testInvalidAlgorithmThrowsException(): void { $table = new Table('invalid_algo', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2975,7 +2961,7 @@ public function testInvalidAlgorithmThrowsException() ])->update(); } - public function testInvalidLockThrowsException() + public function testInvalidLockThrowsException(): void { $table = new Table('invalid_lock', [], $this->adapter); $table->addColumn('col1', 'string') @@ -2989,7 +2975,7 @@ public function testInvalidLockThrowsException() ])->update(); } - public function testAlgorithmInstantWithExplicitLockThrowsException() + public function testAlgorithmInstantWithExplicitLockThrowsException(): void { $table = new Table('instant_lock_test', [], $this->adapter); $table->addColumn('col1', 'string') @@ -3005,7 +2991,7 @@ public function testAlgorithmInstantWithExplicitLockThrowsException() ])->update(); } - public function testAlgorithmConstantsAreDefined() + public function testAlgorithmConstantsAreDefined(): void { $this->assertEquals('DEFAULT', MysqlAdapter::ALGORITHM_DEFAULT); $this->assertEquals('INSTANT', MysqlAdapter::ALGORITHM_INSTANT); @@ -3013,7 +2999,7 @@ public function testAlgorithmConstantsAreDefined() $this->assertEquals('COPY', MysqlAdapter::ALGORITHM_COPY); } - public function testLockConstantsAreDefined() + public function testLockConstantsAreDefined(): void { $this->assertEquals('DEFAULT', MysqlAdapter::LOCK_DEFAULT); $this->assertEquals('NONE', MysqlAdapter::LOCK_NONE); @@ -3021,7 +3007,7 @@ public function testLockConstantsAreDefined() $this->assertEquals('EXCLUSIVE', MysqlAdapter::LOCK_EXCLUSIVE); } - public function testAlgorithmWithMixedCase() + public function testAlgorithmWithMixedCase(): void { $table = new Table('mixed_case', [], $this->adapter); $table->addColumn('col1', 'string') @@ -3037,7 +3023,7 @@ public function testAlgorithmWithMixedCase() $this->assertTrue($this->adapter->hasColumn('mixed_case', 'col2')); } - public function testInsertOrUpdateWithDuplicates() + public function testInsertOrUpdateWithDuplicates(): void { $table = new Table('currencies', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 3]) @@ -3070,7 +3056,7 @@ public function testInsertOrUpdateWithDuplicates() $this->assertEquals('1.0500', $rows[2]['rate']); // USD updated } - public function testInsertOrUpdateWithMultipleUpdateColumns() + public function testInsertOrUpdateWithMultipleUpdateColumns(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('sku', 'string', ['limit' => 50]) @@ -3095,7 +3081,7 @@ public function testInsertOrUpdateWithMultipleUpdateColumns() $this->assertEquals(50, $rows[0]['stock']); } - public function testInsertOrUpdateModeResetsAfterSave() + public function testInsertOrUpdateModeResetsAfterSave(): void { $table = new Table('items', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 10]) @@ -3115,7 +3101,7 @@ public function testInsertOrUpdateModeResetsAfterSave() ])->save(); } - public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn() + public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn(): void { $table = new Table('currencies', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 3]) @@ -3124,7 +3110,7 @@ public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn() ->create(); $warning = null; - set_error_handler(function (int $errno, string $errstr) use (&$warning) { + set_error_handler(function (int $errno, string $errstr) use (&$warning): true { $warning = $errstr; return true; @@ -3147,7 +3133,7 @@ public function testInsertOrUpdateWithEmptyConflictColumnsDoesNotWarn() $this->assertEquals('1.0000', $rows[1]['rate']); } - public function testCreateTableWithRangeColumnsPartitioning() + public function testCreateTableWithRangeColumnsPartitioning(): void { // MySQL requires RANGE COLUMNS for DATE columns $table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter); @@ -3165,7 +3151,7 @@ public function testCreateTableWithRangeColumnsPartitioning() $this->assertTrue($this->adapter->hasColumn('partitioned_orders', 'order_date')); } - public function testCreateTableWithListColumnsPartitioning() + public function testCreateTableWithListColumnsPartitioning(): void { // MySQL requires LIST COLUMNS for STRING columns $table = new Table('partitioned_customers', ['id' => false, 'primary_key' => ['id', 'region']], $this->adapter); @@ -3180,7 +3166,7 @@ public function testCreateTableWithListColumnsPartitioning() $this->assertTrue($this->adapter->hasTable('partitioned_customers')); } - public function testCreateTableWithHashPartitioning() + public function testCreateTableWithHashPartitioning(): void { // MySQL requires partition column in primary key $table = new Table('partitioned_sessions', ['id' => false, 'primary_key' => ['id', 'user_id']], $this->adapter); @@ -3193,7 +3179,7 @@ public function testCreateTableWithHashPartitioning() $this->assertTrue($this->adapter->hasTable('partitioned_sessions')); } - public function testCreateTableWithKeyPartitioning() + public function testCreateTableWithKeyPartitioning(): void { $table = new Table('partitioned_cache', ['id' => false, 'primary_key' => ['cache_key']], $this->adapter); $table->addColumn('cache_key', 'string', ['limit' => 255]) @@ -3204,7 +3190,7 @@ public function testCreateTableWithKeyPartitioning() $this->assertTrue($this->adapter->hasTable('partitioned_cache')); } - public function testCreateTableWithRangePartitioningByInteger() + public function testCreateTableWithRangePartitioningByInteger(): void { $table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter); $table->addColumn('id', 'biginteger') @@ -3218,7 +3204,7 @@ public function testCreateTableWithRangePartitioningByInteger() $this->assertTrue($this->adapter->hasTable('partitioned_logs')); } - public function testCreateTableWithExpressionPartitioning() + public function testCreateTableWithExpressionPartitioning(): void { $table = new Table('partitioned_events', ['id' => false, 'primary_key' => ['id', 'created_at']], $this->adapter); $table->addColumn('id', 'integer') @@ -3232,7 +3218,7 @@ public function testCreateTableWithExpressionPartitioning() $this->assertTrue($this->adapter->hasTable('partitioned_events')); } - public function testAddSinglePartitionToExistingTable() + public function testAddSinglePartitionToExistingTable(): void { // Create a partitioned table with room to add more partitions $table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter); @@ -3260,7 +3246,7 @@ public function testAddSinglePartitionToExistingTable() $this->assertCount(1, $rows); } - public function testAddMultiplePartitionsToExistingTable() + public function testAddMultiplePartitionsToExistingTable(): void { // Create a partitioned table with room to add more partitions $table = new Table('partitioned_sales', ['id' => false, 'primary_key' => ['id', 'sale_date']], $this->adapter); @@ -3297,7 +3283,7 @@ public function testAddMultiplePartitionsToExistingTable() $this->assertCount(3, $rows); } - public function testDropSinglePartitionFromExistingTable() + public function testDropSinglePartitionFromExistingTable(): void { // Create a partitioned table with multiple partitions $table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter); @@ -3334,7 +3320,7 @@ public function testDropSinglePartitionFromExistingTable() $this->assertCount(1, $rows); } - public function testDropMultiplePartitionsFromExistingTable() + public function testDropMultiplePartitionsFromExistingTable(): void { // Create a partitioned table with multiple partitions $table = new Table('partitioned_archive', ['id' => false, 'primary_key' => ['id']], $this->adapter); @@ -3377,7 +3363,7 @@ public function testDropMultiplePartitionsFromExistingTable() $this->assertCount(1, $rows); } - public function testAddMultipleListPartitionsToExistingTable() + public function testAddMultipleListPartitionsToExistingTable(): void { // Create a LIST partitioned table $table = new Table('partitioned_regions', ['id' => false, 'primary_key' => ['id', 'region_id']], $this->adapter); @@ -3409,7 +3395,7 @@ public function testAddMultipleListPartitionsToExistingTable() $this->assertCount(2, $rows); } - public function testAddPartitionsWithMaxvalue() + public function testAddPartitionsWithMaxvalue(): void { // Create a partitioned table without MAXVALUE partition $table = new Table('partitioned_data', ['id' => false, 'primary_key' => ['id']], $this->adapter); diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 58c03871e..22ce3bf91 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -27,13 +27,12 @@ class PostgresAdapterTest extends TestCase { - /** - * @var \Migrations\Db\Adapter\PostgresAdapter - */ - private $adapter; + private PostgresAdapter $adapter; private array $config; + private StubConsoleOutput $out; + private ConsoleIo $io; /** @@ -41,7 +40,7 @@ class PostgresAdapterTest extends TestCase * * @return bool */ - private static function isPostgresAvailable() + private function isPostgresAvailable() { static $available; @@ -67,7 +66,7 @@ protected function setUp(): void 'database' => $config['database'], ]; - if (!self::isPostgresAvailable()) { + if (!$this->isPostgresAvailable()) { $this->markTestSkipped('Postgres is not available. Please install php-pdo-pgsql or equivalent package.'); } @@ -110,17 +109,17 @@ private function usingPostgres10(): bool return version_compare($version, '10.0.0', '>='); } - public function testAdapterType() + public function testAdapterType(): void { $this->assertEquals('pgsql', $this->adapter->getAdapterType()); } - public function testConnection() + public function testConnection(): void { $this->assertInstanceOf(Connection::class, $this->adapter->getConnection()); } - public function testCreatingTheSchemaTableOnConnect() + public function testCreatingTheSchemaTableOnConnect(): void { $this->adapter->connect(); $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); @@ -131,21 +130,21 @@ public function testCreatingTheSchemaTableOnConnect() $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); } - public function testSchemaTableIsCreatedWithPrimaryKey() + public function testSchemaTableIsCreatedWithPrimaryKey(): void { $this->adapter->connect(); new Table($this->adapter->getSchemaTableName(), [], $this->adapter); $this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version'])); } - public function testQuoteSchemaName() + public function testQuoteSchemaName(): void { $this->assertEquals('"schema"', $this->adapter->quoteSchemaName('schema')); // No . is supported in schema name. $this->assertEquals('"schema"."schema"', $this->adapter->quoteSchemaName('schema.schema')); } - public function testGetGlobalSchemaName() + public function testGetGlobalSchemaName(): void { $config = ConnectionManager::getConfig('test'); $config['schema'] = 'test_schema'; @@ -169,20 +168,20 @@ public function testGetGlobalSchemaName() ConnectionManager::drop('test-schema'); } - public function testQuoteTableName() + public function testQuoteTableName(): void { $this->assertEquals('"public"."table"', $this->adapter->quoteTableName('table')); $this->assertEquals('"table"."table"', $this->adapter->quoteTableName('table.table')); } - public function testQuoteColumnName() + public function testQuoteColumnName(): void { $this->assertEquals('"string"', $this->adapter->quoteColumnName('string')); // No . is supported in column name. $this->assertEquals('"string"."string"', $this->adapter->quoteColumnName('string.string')); } - public function testCreateTable() + public function testCreateTable(): void { $table = new Table('ntable', [], $this->adapter); $table->addColumn('realname', 'string') @@ -195,7 +194,7 @@ public function testCreateTable() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableWithSchema() + public function testCreateTableWithSchema(): void { $this->adapter->createSchema('nschema'); @@ -212,7 +211,7 @@ public function testCreateTableWithSchema() $this->adapter->dropSchema('nschema'); } - public function testCreateTableCustomIdColumn() + public function testCreateTableCustomIdColumn(): void { $table = new Table('ntable', ['id' => 'custom_id'], $this->adapter); $table->addColumn('realname', 'string') @@ -225,7 +224,7 @@ public function testCreateTableCustomIdColumn() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableWithNoPrimaryKey() + public function testCreateTableWithNoPrimaryKey(): void { $options = [ 'id' => false, @@ -236,7 +235,7 @@ public function testCreateTableWithNoPrimaryKey() $this->assertFalse($this->adapter->hasColumn('atable', 'id')); } - public function testCreateTableWithConflictingPrimaryKeys() + public function testCreateTableWithConflictingPrimaryKeys(): void { $options = [ 'primary_key' => 'user_id', @@ -248,7 +247,7 @@ public function testCreateTableWithConflictingPrimaryKeys() $table->addColumn('user_id', 'integer')->save(); } - public function testCreateTableWithPrimaryKeySetToImplicitId() + public function testCreateTableWithPrimaryKeySetToImplicitId(): void { $options = [ 'primary_key' => 'id', @@ -260,7 +259,7 @@ public function testCreateTableWithPrimaryKeySetToImplicitId() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithPrimaryKeyArraySetToImplicitId() + public function testCreateTableWithPrimaryKeyArraySetToImplicitId(): void { $options = [ 'primary_key' => ['id'], @@ -272,7 +271,7 @@ public function testCreateTableWithPrimaryKeyArraySetToImplicitId() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId() + public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId(): void { $options = [ 'primary_key' => ['id', 'user_id'], @@ -283,7 +282,7 @@ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId() $table->addColumn('user_id', 'integer')->save(); } - public function testCreateTableWithMultiplePrimaryKeys() + public function testCreateTableWithMultiplePrimaryKeys(): void { $options = [ 'id' => false, @@ -298,7 +297,7 @@ public function testCreateTableWithMultiplePrimaryKeys() $this->assertFalse($this->adapter->hasIndex('table1', ['tag_id', 'user_email'])); } - public function testCreateTableWithMultiplePrimaryKeysWithSchema() + public function testCreateTableWithMultiplePrimaryKeysWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -320,7 +319,7 @@ public function testCreateTableWithMultiplePrimaryKeysWithSchema() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsUuid() + public function testCreateTableWithPrimaryKeyAsUuid(): void { $options = [ 'id' => false, @@ -337,7 +336,7 @@ public function testCreateTableWithPrimaryKeyAsUuid() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsBinaryUuid() + public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void { $options = [ 'id' => false, @@ -354,7 +353,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsNativeUuid() + public function testCreateTableWithPrimaryKeyAsNativeUuid(): void { $options = [ 'id' => false, @@ -368,7 +367,7 @@ public function testCreateTableWithPrimaryKeyAsNativeUuid() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithMultipleIndexes() + public function testCreateTableWithMultipleIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -382,7 +381,7 @@ public function testCreateTableWithMultipleIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name'])); } - public function testCreateTableWithUniqueIndexes() + public function testCreateTableWithUniqueIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -392,7 +391,7 @@ public function testCreateTableWithUniqueIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email'])); } - public function testCreateTableWithNamedIndexes() + public function testCreateTableWithNamedIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -465,7 +464,7 @@ public function testCreateTableIndexWithWhere(): void $this->assertStringContainsString('("email") WHERE is_verified = true', $indexQuery); } - public function testAddPrimaryKey() + public function testAddPrimaryKey(): void { $table = new Table('table1', ['id' => false], $this->adapter); $table @@ -479,7 +478,7 @@ public function testAddPrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testChangePrimaryKey() + public function testChangePrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -496,7 +495,7 @@ public function testChangePrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2', 'column3'])); } - public function testDropPrimaryKey() + public function testDropPrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -510,7 +509,7 @@ public function testDropPrimaryKey() $this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testAddComment() + public function testAddComment(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -531,7 +530,7 @@ public function testAddComment() $this->assertEquals('comment1', $rows[0]['description']); } - public function testChangeComment() + public function testChangeComment(): void { $table = new Table('table1', ['comment' => 'comment1'], $this->adapter); $table->save(); @@ -552,7 +551,7 @@ public function testChangeComment() $this->assertEquals('comment2', $rows[0]['description']); } - public function testDropComment() + public function testDropComment(): void { $table = new Table('table1', ['comment' => 'comment1'], $this->adapter); $table->save(); @@ -573,7 +572,7 @@ public function testDropComment() $this->assertEmpty($rows); } - public function testRenameTable() + public function testRenameTable(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -585,7 +584,7 @@ public function testRenameTable() $this->assertTrue($this->adapter->hasTable('table2')); } - public function testRenameTableWithSchema() + public function testRenameTableWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -600,7 +599,7 @@ public function testRenameTableWithSchema() $this->adapter->dropSchema('schema1'); } - public function testAddColumn() + public function testAddColumn(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -610,7 +609,7 @@ public function testAddColumn() $this->assertTrue($table->hasColumn('email')); } - public function testAddColumnWithDefaultValue() + public function testAddColumnWithDefaultValue(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -624,7 +623,7 @@ public function testAddColumnWithDefaultValue() } } - public function testAddColumnWithDefaultZero() + public function testAddColumnWithDefaultZero(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -639,13 +638,14 @@ public function testAddColumnWithDefaultZero() } } - public function testAddColumnWithAutoIdentity() + public function testAddColumnWithAutoIdentity(): void { if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } $table = new Table('table1', [], $this->adapter); $table->save(); + $columns = $this->adapter->getColumns('table1'); foreach ($columns as $column) { if ($column->getName() === 'id') { @@ -658,7 +658,7 @@ public function testAddColumnWithAutoIdentity() /** * Test that shims from PHINX_TYPE_JSONB to 'json' type work. */ - public function testAddColumnJsonbCompat() + public function testAddColumnJsonbCompat(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -678,15 +678,16 @@ public static function providerAddColumnIdentity(): array } #[DataProvider('providerAddColumnIdentity')] - public function testAddColumnIdentity($generated, $addToColumn) + public function testAddColumnIdentity(string $generated, bool $addToColumn): void { if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); } $table = new Table('table1', ['id' => false], $this->adapter); $table->save(); + $options = ['identity' => true]; - if ($addToColumn !== false) { + if ($addToColumn) { $options['generated'] = $generated; } $table->addColumn('id', 'integer', $options) @@ -700,7 +701,7 @@ public function testAddColumnIdentity($generated, $addToColumn) } } - public function testAddColumnWithDefaultBoolean() + public function testAddColumnWithDefaultBoolean(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -724,7 +725,7 @@ public function testAddColumnWithDefaultBoolean() } } - public function testAddColumnWithBooleanIgnoreLimitCastDefault() + public function testAddColumnWithBooleanIgnoreLimitCastDefault(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -758,7 +759,7 @@ public function testAddColumnWithBooleanIgnoreLimitCastDefault() $this->assertNull($column->getLimit()); } - public function testAddColumnWithComment() + public function testAddColumnWithComment(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -788,7 +789,7 @@ public function testAddColumnWithComment() ); } - public function testAddStringWithLimit() + public function testAddStringWithLimit(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -807,7 +808,7 @@ public function testAddStringWithLimit() } } - public function testAddDecimalWithPrecisionAndScale() + public function testAddDecimalWithPrecisionAndScale(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -829,7 +830,7 @@ public function testAddDecimalWithPrecisionAndScale() } } - public function testAddTimestampWithPrecision() + public function testAddTimestampWithPrecision(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -853,7 +854,7 @@ public function testAddTimestampWithPrecision() } } - public function testRenameColumn() + public function testRenameColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -865,7 +866,7 @@ public function testRenameColumn() $this->assertTrue($this->adapter->hasColumn('t', 'column2')); } - public function testRenameColumnIsCaseSensitive() + public function testRenameColumnIsCaseSensitive(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('columnOne', 'string') @@ -877,7 +878,7 @@ public function testRenameColumnIsCaseSensitive() $this->assertTrue($this->adapter->hasColumn('t', 'columnTwo')); } - public function testRenamingANonExistentColumn() + public function testRenamingANonExistentColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -890,13 +891,13 @@ public function testRenamingANonExistentColumn() $this->assertInstanceOf( 'InvalidArgumentException', $e, - 'Expected exception of type InvalidArgumentException, got ' . get_class($e), + 'Expected exception of type InvalidArgumentException, got ' . $e::class, ); $this->assertEquals('The specified column does not exist: column2', $e->getMessage()); } } - public function testChangeColumn() + public function testChangeColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -922,7 +923,7 @@ public static function providerChangeColumnIdentity(): array } #[DataProvider('providerChangeColumnIdentity')] - public function testChangeColumnIdentity($generated) + public function testChangeColumnIdentity(string $generated): void { if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); @@ -933,6 +934,7 @@ public function testChangeColumnIdentity($generated) $table->changeColumn('column1', 'integer', ['identity' => true, 'generated' => PostgresAdapter::GENERATED_ALWAYS]); $table->save(); + $columns = $this->adapter->getColumns('table1'); foreach ($columns as $column) { if ($column->getName() === 'column1') { @@ -942,7 +944,7 @@ public function testChangeColumnIdentity($generated) } } - public function testChangeColumnDropIdentity() + public function testChangeColumnDropIdentity(): void { if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); @@ -951,6 +953,7 @@ public function testChangeColumnDropIdentity() $table->save(); $table->changeColumn('id', 'integer', ['identity' => false]); $table->save(); + $columns = $this->adapter->getColumns('table1'); foreach ($columns as $column) { if ($column->getName() === 'id') { @@ -959,7 +962,7 @@ public function testChangeColumnDropIdentity() } } - public function testChangeColumnChangeIdentity() + public function testChangeColumnChangeIdentity(): void { if (!$this->usingPostgres10()) { $this->markTestSkipped('Test Skipped because of PostgreSQL version is < 10.0'); @@ -968,6 +971,7 @@ public function testChangeColumnChangeIdentity() $table->save(); $table->changeColumn('id', 'integer', ['identity' => true, 'generated' => PostgresAdapter::GENERATED_BY_DEFAULT]); $table->save(); + $columns = $this->adapter->getColumns('table1'); foreach ($columns as $column) { if ($column->getName() === 'id') { @@ -977,7 +981,7 @@ public function testChangeColumnChangeIdentity() } } - public static function integersProvider() + public static function integersProvider(): array { return [ ['smallinteger', 32767], @@ -987,7 +991,7 @@ public static function integersProvider() } #[DataProvider('integersProvider')] - public function testChangeColumnFromTextToInteger($type, $value) + public function testChangeColumnFromTextToInteger(string $type, int $value): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'text') @@ -1002,7 +1006,7 @@ public function testChangeColumnFromTextToInteger($type, $value) $this->assertSame($value, $row['column1']); } - public function testChangeBooleanOptions() + public function testChangeBooleanOptions(): void { $table = new Table('t', ['id' => false], $this->adapter); $table->addColumn('my_bool', 'boolean', ['default' => true, 'null' => true]) @@ -1020,12 +1024,12 @@ public function testChangeBooleanOptions() $rows = $this->adapter->fetchAll('SELECT * FROM t'); $this->assertCount(3, $rows); - $this->assertSame([true, false, null], array_map(function ($row) { + $this->assertSame([true, false, null], array_map(function (array $row) { return $row['my_bool']; }, $rows)); } - public function testChangeColumnFromIntegerToBoolean() + public function testChangeColumnFromIntegerToBoolean(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'integer', ['default' => 0]) @@ -1041,7 +1045,7 @@ public function testChangeColumnFromIntegerToBoolean() } } - public function testChangeColumnCharToUuid() + public function testChangeColumnCharToUuid(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'char', ['default' => null, 'limit' => 36]) @@ -1059,7 +1063,7 @@ public function testChangeColumnCharToUuid() } } - public function testChangeColumnCharToNativeUuid() + public function testChangeColumnCharToNativeUuid(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'char', ['default' => null, 'limit' => 36]) @@ -1077,7 +1081,7 @@ public function testChangeColumnCharToNativeUuid() } } - public function testChangeColumnWithDefault() + public function testChangeColumnWithDefault(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1100,7 +1104,7 @@ public function testChangeColumnWithDefault() } } - public function testChangeColumnWithDropDefault() + public function testChangeColumnWithDropDefault(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'Test']) @@ -1127,7 +1131,7 @@ public function testChangeColumnWithDropDefault() } } - public function testDropColumn() + public function testDropColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1138,7 +1142,7 @@ public function testDropColumn() $this->assertFalse($this->adapter->hasColumn('t', 'column1')); } - public static function columnsProvider() + public static function columnsProvider(): array { return [ ['column1', 'string', []], @@ -1161,7 +1165,7 @@ public static function columnsProvider() } #[DataProvider('columnsProvider')] - public function testGetColumns($colName, $type, $options, $actualType = null) + public function testGetColumns(string $colName, string $type, array $options, ?string $actualType = null): void { $table = new Table('t', [], $this->adapter); $table->addColumn($colName, $type, $options)->save(); @@ -1182,7 +1186,7 @@ public function testGetColumns($colName, $type, $options, $actualType = null) } #[DataProvider('columnsProvider')] - public function testGetColumnsWithSchema($colName, $type, $options, $actualType = null) + public function testGetColumnsWithSchema(string $colName, string $type, array $options, ?string $actualType = null): void { $this->adapter->createSchema('tschema'); @@ -1206,7 +1210,7 @@ public function testGetColumnsWithSchema($colName, $type, $options, $actualType $this->adapter->dropSchema('tschema'); } - public function testAddIndex() + public function testAddIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1217,7 +1221,7 @@ public function testAddIndex() $this->assertTrue($table->hasIndex('email')); } - public function testAddIndexWithSort() + public function testAddIndexWithSort(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1259,7 +1263,7 @@ public function testAddIndexWithSort() $this->assertEquals($emailOrder['sort_order'], 'ASC'); } - public function testAddIndexWithIncludeColumns() + public function testAddIndexWithIncludeColumns(): void { if (!version_compare($this->adapter->fetchAll('SHOW server_version;')[0]['server_version'], '11.0.0', '>=')) { $this->markTestSkipped('Cannot test index include collumns (non-key columns) on postgresql versions less than 11'); @@ -1312,7 +1316,7 @@ public function testAddIndexWithIncludeColumns() $this->assertEquals($indexColumn['index_column'], 'INCLUDED'); } - public function testAddIndexWithSchema() + public function testAddIndexWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -1327,7 +1331,7 @@ public function testAddIndexWithSchema() $this->adapter->dropSchema('schema1'); } - public function testAddIndexWithNameWithSchema() + public function testAddIndexWithNameWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -1342,7 +1346,7 @@ public function testAddIndexWithNameWithSchema() $this->adapter->dropSchema('schema1'); } - public function testAddIndexIsCaseSensitive() + public function testAddIndexIsCaseSensitive(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('theEmail', 'string') @@ -1353,7 +1357,7 @@ public function testAddIndexIsCaseSensitive() $this->assertTrue($table->hasIndex('theEmail')); } - public function testDropIndex() + public function testDropIndex(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -1394,7 +1398,7 @@ public function testDropIndex() $this->assertFalse($table4->hasIndex(['fname', 'lname'])); } - public function testDropIndexWithSchema() + public function testDropIndexWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -1439,7 +1443,7 @@ public function testDropIndexWithSchema() $this->adapter->dropSchema('schema1'); } - public function testDropIndexByName() + public function testDropIndexByName(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -1464,7 +1468,7 @@ public function testDropIndexByName() $this->assertFalse($table2->hasIndex(['fname', 'lname'])); } - public function testDropIndexByNameWithSchema() + public function testDropIndexByNameWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -1493,7 +1497,7 @@ public function testDropIndexByNameWithSchema() $this->adapter->dropSchema('schema1'); } - public function testAddForeignKey() + public function testAddForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1507,7 +1511,7 @@ public function testAddForeignKey() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testAddForeignKeyWithSchema() + public function testAddForeignKeyWithSchema(): void { $this->adapter->createSchema('schema1'); $this->adapter->createSchema('schema2'); @@ -1527,7 +1531,7 @@ public function testAddForeignKeyWithSchema() $this->adapter->dropSchema('schema2'); } - public function testAddForeignKeyDeferrable() + public function testAddForeignKeyDeferrable(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1548,7 +1552,7 @@ public function testAddForeignKeyDeferrable() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKey() + public function testDropForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1563,7 +1567,7 @@ public function testDropForeignKey() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyWithMultipleColumns() + public function testDropForeignKeyWithMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1614,7 +1618,7 @@ public function testDropForeignKeyWithMultipleColumns() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_field1', 'ref_table_id'])); } - public function testDropForeignKeyWithIdenticalMultipleColumns() + public function testDropForeignKeyWithIdenticalMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1662,11 +1666,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array ]; } - /** - * @param array $columns - */ #[DataProvider('nonExistentForeignKeyColumnsProvider')] - public function testDropForeignKeyByNonExistentKeyColumns(array $columns) + public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1694,7 +1695,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns) $this->adapter->dropForeignKey($table->getName(), $columns); } - public function testDropForeignKeyCaseSensitivity() + public function testDropForeignKeyCaseSensitivity(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1716,7 +1717,7 @@ public function testDropForeignKeyCaseSensitivity() $this->adapter->dropForeignKey($table->getName(), ['ref_table_id']); } - public function testDropForeignKeyByName() + public function testDropForeignKeyByName(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1737,7 +1738,7 @@ public function testDropForeignKeyByName() } #[DataProvider('provideForeignKeysToCheck')] - public function testHasForeignKey($tableDef, $key, $exp) + public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void { $conn = $this->adapter->getConnection(); $conn->execute('CREATE TABLE other(a int, b int, c int, unique(a), unique(b), unique(a,b), unique(a,b,c));'); @@ -1745,7 +1746,7 @@ public function testHasForeignKey($tableDef, $key, $exp) $this->assertSame($exp, $this->adapter->hasForeignKey('t', $key)); } - public static function provideForeignKeysToCheck() + public static function provideForeignKeysToCheck(): array { return [ ['create table t(a int)', 'a', false], @@ -1771,7 +1772,7 @@ public static function provideForeignKeysToCheck() ]; } - public function testHasNamedForeignKey() + public function testHasNamedForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1794,7 +1795,7 @@ public function testHasNamedForeignKey() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'my_constraint2')); } - public function testDropForeignKeyWithSchema() + public function testDropForeignKeyWithSchema(): void { $this->adapter->createSchema('schema1'); $this->adapter->createSchema('schema2'); @@ -1815,7 +1816,7 @@ public function testDropForeignKeyWithSchema() $this->adapter->dropSchema('schema2'); } - public function testDropForeignKeyNotDroppingPrimaryKey() + public function testDropForeignKeyNotDroppingPrimaryKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1833,13 +1834,13 @@ public function testDropForeignKeyNotDroppingPrimaryKey() $this->assertTrue($this->adapter->hasIndexByName('table', 'table_pkey')); } - public function testHasDatabase() + public function testHasDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('fake_database_name')); $this->assertTrue($this->adapter->hasDatabase($this->config['database'])); } - public function testDropDatabase() + public function testDropDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('phinx_temp_database')); $this->adapter->createDatabase('phinx_temp_database'); @@ -1847,13 +1848,13 @@ public function testDropDatabase() $this->adapter->dropDatabase('phinx_temp_database'); } - public function testCreateSchema() + public function testCreateSchema(): void { $this->adapter->createSchema('foo'); $this->assertTrue($this->adapter->hasSchema('foo')); } - public function testDropSchema() + public function testDropSchema(): void { $this->adapter->createSchema('foo'); $this->assertTrue($this->adapter->hasSchema('foo')); @@ -1861,7 +1862,7 @@ public function testDropSchema() $this->assertFalse($this->adapter->hasSchema('foo')); } - public function testDropAllSchemas() + public function testDropAllSchemas(): void { $this->adapter->createSchema('foo'); $this->adapter->createSchema('bar'); @@ -1873,7 +1874,7 @@ public function testDropAllSchemas() $this->assertFalse($this->adapter->hasSchema('bar')); } - public function testCreateTableWithComment() + public function testCreateTableWithComment(): void { $tableComment = 'Table comment'; $table = new Table('ntable', ['comment' => $tableComment], $this->adapter); @@ -1895,7 +1896,7 @@ public function testCreateTableWithComment() $this->assertEquals($tableComment, $rows[0]['description'], 'Dont set table comment correctly'); } - public function testCanAddColumnComment() + public function testCanAddColumnComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn( @@ -1918,7 +1919,7 @@ public function testCanAddColumnComment() $this->assertEquals($comment, $row['column_comment'], 'Dont set column comment correctly'); } - public function testCanAddCommentForColumnWithReservedName() + public function testCanAddCommentForColumnWithReservedName(): void { $table = new Table('user', [], $this->adapter); $table->addColumn('index', 'string', ['comment' => $comment = 'Comments from column "index"']) @@ -1943,7 +1944,7 @@ public function testCanAddCommentForColumnWithReservedName() } #[Depends('testCanAddColumnComment')] - public function testCanChangeColumnComment() + public function testCanChangeColumnComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"']) @@ -1970,7 +1971,7 @@ public function testCanChangeColumnComment() } #[Depends('testCanAddColumnComment')] - public function testCanRemoveColumnComment() + public function testCanRemoveColumnComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"']) @@ -1994,7 +1995,7 @@ public function testCanRemoveColumnComment() } #[Depends('testCanAddColumnComment')] - public function testCanAddMultipleCommentsToOneTable() + public function testCanAddMultipleCommentsToOneTable(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('comment1', 'string', [ @@ -2033,7 +2034,7 @@ public function testCanAddMultipleCommentsToOneTable() } #[Depends('testCanAddColumnComment')] - public function testColumnsAreResetBetweenTables() + public function testColumnsAreResetBetweenTables(): void { $table = new Table('widgets', [], $this->adapter); $table->addColumn('transport', 'string', [ @@ -2062,7 +2063,7 @@ public function testColumnsAreResetBetweenTables() /** * Test that column names are properly escaped when creating Foreign Keys */ - public function testForeignKeysAreProperlyEscaped() + public function testForeignKeysAreProperlyEscaped(): void { $userId = 'user'; $sessionId = 'session'; @@ -2082,7 +2083,7 @@ public function testForeignKeysAreProperlyEscaped() $this->assertTrue($foreign->hasForeignKey('user')); } - public function testForeignKeysAreProperlyEscapedWithSchema() + public function testForeignKeysAreProperlyEscapedWithSchema(): void { $this->adapter->createSchema('schema_users'); @@ -2110,7 +2111,7 @@ public function testForeignKeysAreProperlyEscapedWithSchema() $this->adapter->dropSchema('schema_users'); } - public function testForeignKeysAreProperlyEscapedWithSchema2() + public function testForeignKeysAreProperlyEscapedWithSchema2(): void { $this->adapter->createSchema('schema_users'); $this->adapter->createSchema('schema_sessions'); @@ -2140,7 +2141,7 @@ public function testForeignKeysAreProperlyEscapedWithSchema2() $this->adapter->dropSchema('schema_sessions'); } - public function testTimestampWithTimezone() + public function testTimestampWithTimezone(): void { $table = new Table('tztable', ['id' => false], $this->adapter); $table @@ -2154,7 +2155,7 @@ public function testTimestampWithTimezone() $columns = $this->adapter->getColumns('tztable'); foreach ($columns as $column) { - if (substr($column->getName(), -4) === 'notz') { + if (str_ends_with((string)$column->getName(), 'notz')) { $this->assertFalse($column->isTimezone(), 'column: ' . $column->getName()); } else { $this->assertTrue($column->isTimezone(), 'column: ' . $column->getName()); @@ -2162,7 +2163,7 @@ public function testTimestampWithTimezone() } } - public function testTimestampWithTimezoneWithSchema() + public function testTimestampWithTimezoneWithSchema(): void { $this->adapter->createSchema('tzschema'); @@ -2178,7 +2179,7 @@ public function testTimestampWithTimezoneWithSchema() $columns = $this->adapter->getColumns('tzschema.tztable'); foreach ($columns as $column) { - if (substr($column->getName(), -4) === 'notz') { + if (str_ends_with((string)$column->getName(), 'notz')) { $this->assertFalse($column->isTimezone(), 'column: ' . $column->getName()); } else { $this->assertTrue($column->isTimezone(), 'column: ' . $column->getName()); @@ -2188,7 +2189,7 @@ public function testTimestampWithTimezoneWithSchema() $this->adapter->dropSchema('tzschema'); } - public function testBulkInsertData() + public function testBulkInsertData(): void { $data = [ [ @@ -2222,7 +2223,7 @@ public function testBulkInsertData() $this->assertEquals('test', $rows[2]['column3']); } - public function testBulkInsertBoolean() + public function testBulkInsertBoolean(): void { $data = [ [ @@ -2246,7 +2247,7 @@ public function testBulkInsertBoolean() $this->assertNull($rows[2]['column1']); } - public function testBulkInsertLiteral() + public function testBulkInsertLiteral(): void { $data = [ [ @@ -2272,12 +2273,12 @@ public function testBulkInsertLiteral() $this->assertEquals('value1', $rows[0]['column1']); $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']); $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); } - public function testInsertData() + public function testInsertData(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -2301,7 +2302,7 @@ public function testInsertData() $this->assertEquals(2, $rows[1]['column2']); } - public function testInsertLiteral() + public function testInsertLiteral(): void { $data = [ [ @@ -2332,12 +2333,12 @@ public function testInsertLiteral() $this->assertEquals('test', $rows[0]['column2']); $this->assertEquals('test', $rows[1]['column2']); $this->assertEquals('foo', $rows[2]['column2']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']); $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); } - public function testInsertBoolean() + public function testInsertBoolean(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'boolean', ['null' => true]) @@ -2362,7 +2363,7 @@ public function testInsertBoolean() $this->assertNull($rows[2]['column1']); } - public function testInsertDataWithSchema() + public function testInsertDataWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -2390,7 +2391,7 @@ public function testInsertDataWithSchema() $this->adapter->dropSchema('schema1'); } - public function testTruncateTable() + public function testTruncateTable(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -2414,7 +2415,7 @@ public function testTruncateTable() $this->assertCount(0, $rows); } - public function testTruncateTableWithSchema() + public function testTruncateTableWithSchema(): void { $this->adapter->createSchema('schema1'); @@ -2442,7 +2443,7 @@ public function testTruncateTableWithSchema() $this->adapter->dropSchema('schema1'); } - public function testDumpCreateTable() + public function testDumpCreateTable(): void { $options = $this->adapter->getOptions(); $options['dryrun'] = true; @@ -2455,7 +2456,7 @@ public function testDumpCreateTable() ->addColumn('column3', 'string', ['default' => 'test', 'null' => false]) ->save(); - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); // Check for key parts of the CREATE TABLE statement // The identity column syntax varies between CakePHP/database versions $this->assertStringContainsString( @@ -2469,7 +2470,7 @@ public function testDumpCreateTable() $this->assertStringContainsString('CONSTRAINT "table1_pkey" PRIMARY KEY ("id")', $actualOutput); } - public function testDumpCreateTableWithSchema() + public function testDumpCreateTableWithSchema(): void { $options = $this->adapter->getOptions(); $options['dryrun'] = true; @@ -2482,7 +2483,7 @@ public function testDumpCreateTableWithSchema() ->addColumn('column3', 'string', ['default' => 'test', 'null' => false]) ->save(); - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); // Check for key parts of the CREATE TABLE statement // The identity column syntax varies between CakePHP/database versions $this->assertStringContainsString( @@ -2501,7 +2502,7 @@ public function testDumpCreateTableWithSchema() * Then enables dry run mode and inserts a record. * Asserts that output contains the insert statement and doesn't insert a record. */ - public function testDumpInsert() + public function testDumpInsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2538,11 +2539,11 @@ public function testDumpInsert() OUTPUT; } - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); $this->assertStringContainsString( $expectedOutput, $actualOutput, - 'Passing the --dry-run option doesn\'t dump the insert to the output', + "Passing the --dry-run option doesn't dump the insert to the output", ); $countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1'); @@ -2556,7 +2557,7 @@ public function testDumpInsert() * Then enables dry run mode and inserts some records. * Asserts that output contains the insert statement and doesn't insert any record. */ - public function testDumpBulkinsert() + public function testDumpBulkinsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2588,11 +2589,11 @@ public function testDumpBulkinsert() OUTPUT; } - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); $this->assertStringContainsString( $expectedOutput, $actualOutput, - 'Passing the --dry-run option doesn\'t dump the bulkinsert to the output', + "Passing the --dry-run option doesn't dump the bulkinsert to the output", ); $countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1'); @@ -2601,7 +2602,7 @@ public function testDumpBulkinsert() $this->assertEquals(0, $res[0]['count']); } - public function testDumpCreateTableAndThenInsert() + public function testDumpCreateTableAndThenInsert(): void { $options = $this->adapter->getOptions(); $options['dryrun'] = true; @@ -2630,14 +2631,14 @@ public function testDumpCreateTableAndThenInsert() OUTPUT; } - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create and then insert table queries to the output'); } /** * Tests interaction with the query builder */ - public function testQueryBuilder() + public function testQueryBuilder(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2676,7 +2677,7 @@ public function testQueryBuilder() $this->assertEquals(1, $stm->rowCount()); } - public function testQueryWithParams() + public function testQueryWithParams(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2707,7 +2708,7 @@ public function testQueryWithParams() $this->assertEquals(3, $res[0]['c']); } - public function testRenameMixedCaseTableAndColumns() + public function testRenameMixedCaseTableAndColumns(): void { $table = new Table('OrganizationSettings', [], $this->adapter); $table->addColumn('SettingType', 'string') @@ -2752,7 +2753,7 @@ public function testSerialAliases(string $columnType): void $this->assertFalse($column->isNull()); } - public function testAddCheckConstraint() + public function testAddCheckConstraint(): void { $table = new Table('check_table', [], $this->adapter); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) @@ -2764,7 +2765,7 @@ public function testAddCheckConstraint() $this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive')); } - public function testHasCheckConstraint() + public function testHasCheckConstraint(): void { $table = new Table('check_table3', [], $this->adapter); $table->addColumn('quantity', 'integer') @@ -2778,7 +2779,7 @@ public function testHasCheckConstraint() $this->assertTrue($this->adapter->hasCheckConstraint('check_table3', 'quantity_positive')); } - public function testDropCheckConstraint() + public function testDropCheckConstraint(): void { $table = new Table('check_table4', [], $this->adapter); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) @@ -2792,7 +2793,7 @@ public function testDropCheckConstraint() $this->assertFalse($this->adapter->hasCheckConstraint('check_table4', 'price_check')); } - public function testCheckConstraintWithComplexExpression() + public function testCheckConstraintWithComplexExpression(): void { $table = new Table('check_table5', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -2809,10 +2810,10 @@ public function testCheckConstraintWithComplexExpression() // Verify the constraint is actually enforced $quotedTableName = $this->adapter->getConnection()->getDriver()->quoteIdentifier('check_table5'); $this->expectException(PDOException::class); - $this->adapter->execute("INSERT INTO {$quotedTableName} (email, status) VALUES ('test@example.com', 'invalid')"); + $this->adapter->execute(sprintf("INSERT INTO %s (email, status) VALUES ('test@example.com', 'invalid')", $quotedTableName)); } - public function testInsertOrSkipWithDuplicates() + public function testInsertOrSkipWithDuplicates(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -2835,7 +2836,7 @@ public function testInsertOrSkipWithDuplicates() $this->assertEquals('John', $rows[0]['name']); } - public function testInsertModeResetsAfterInsertOrSkip() + public function testInsertModeResetsAfterInsertOrSkip(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -2855,7 +2856,7 @@ public function testInsertModeResetsAfterInsertOrSkip() ])->save(); } - public function testBulkinsertOrSkipWithDuplicates() + public function testBulkinsertOrSkipWithDuplicates(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('sku', 'string', ['limit' => 50]) @@ -2882,7 +2883,7 @@ public function testBulkinsertOrSkipWithDuplicates() $this->assertEquals('30.00', $rows[2]['price']); } - public function testInsertOrSkipWithoutDuplicates() + public function testInsertOrSkipWithoutDuplicates(): void { $table = new Table('categories', [], $this->adapter); $table->addColumn('name', 'string') @@ -2898,7 +2899,7 @@ public function testInsertOrSkipWithoutDuplicates() $this->assertCount(2, $rows); } - public function testInsertOrUpdateWithDuplicates() + public function testInsertOrUpdateWithDuplicates(): void { $table = new Table('currencies', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 3]) @@ -2931,7 +2932,7 @@ public function testInsertOrUpdateWithDuplicates() $this->assertEquals('1.0500', $rows[2]['rate']); // USD updated } - public function testInsertOrUpdateWithMultipleUpdateColumns() + public function testInsertOrUpdateWithMultipleUpdateColumns(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('sku', 'string', ['limit' => 50]) @@ -2956,7 +2957,7 @@ public function testInsertOrUpdateWithMultipleUpdateColumns() $this->assertEquals(50, $rows[0]['stock']); } - public function testInsertOrUpdateModeResetsAfterSave() + public function testInsertOrUpdateModeResetsAfterSave(): void { $table = new Table('items', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 10]) @@ -2976,7 +2977,7 @@ public function testInsertOrUpdateModeResetsAfterSave() ])->save(); } - public function testInsertOrUpdateRequiresConflictColumns() + public function testInsertOrUpdateRequiresConflictColumns(): void { $table = new Table('currencies', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 3]) @@ -2992,7 +2993,7 @@ public function testInsertOrUpdateRequiresConflictColumns() ], ['rate'], [])->save(); } - public function testAddSinglePartitionToExistingTable() + public function testAddSinglePartitionToExistingTable(): void { // Create a partitioned table with room to add more partitions $table = new Table('partitioned_orders', ['id' => false, 'primary_key' => ['id', 'order_date']], $this->adapter); @@ -3023,7 +3024,7 @@ public function testAddSinglePartitionToExistingTable() $this->adapter->dropTable('partitioned_orders'); } - public function testAddMultiplePartitionsToExistingTable() + public function testAddMultiplePartitionsToExistingTable(): void { // Create a partitioned table $table = new Table('partitioned_sales', ['id' => false, 'primary_key' => ['id', 'sale_date']], $this->adapter); @@ -3061,7 +3062,7 @@ public function testAddMultiplePartitionsToExistingTable() $this->adapter->dropTable('partitioned_sales'); } - public function testDropSinglePartitionFromExistingTable() + public function testDropSinglePartitionFromExistingTable(): void { // Create a partitioned table with multiple partitions $table = new Table('partitioned_logs', ['id' => false, 'primary_key' => ['id']], $this->adapter); @@ -3103,7 +3104,7 @@ public function testDropSinglePartitionFromExistingTable() $this->adapter->dropTable('partitioned_logs'); } - public function testDropMultiplePartitionsFromExistingTable() + public function testDropMultiplePartitionsFromExistingTable(): void { // Create a partitioned table with multiple partitions $table = new Table('partitioned_archive', ['id' => false, 'primary_key' => ['id']], $this->adapter); diff --git a/tests/TestCase/Db/Adapter/RecordingAdapterTest.php b/tests/TestCase/Db/Adapter/RecordingAdapterTest.php index c5a684323..260979e9e 100644 --- a/tests/TestCase/Db/Adapter/RecordingAdapterTest.php +++ b/tests/TestCase/Db/Adapter/RecordingAdapterTest.php @@ -3,6 +3,12 @@ namespace Migrations\Test\Db\Adapter; +use Migrations\Db\Action\DropForeignKey; +use Migrations\Db\Action\DropIndex; +use Migrations\Db\Action\DropTable; +use Migrations\Db\Action\RemoveColumn; +use Migrations\Db\Action\RenameColumn; +use Migrations\Db\Action\RenameTable; use Migrations\Db\Adapter\AbstractAdapter; use Migrations\Db\Adapter\RecordingAdapter; use Migrations\Db\Table; @@ -12,10 +18,7 @@ class RecordingAdapterTest extends TestCase { - /** - * @var \Migrations\Db\Adapter\RecordingAdapter - */ - private $adapter; + private RecordingAdapter $adapter; protected function setUp(): void { @@ -35,30 +38,30 @@ protected function tearDown(): void unset($this->adapter); } - public function testRecordingAdapterCanInvertCreateTable() + public function testRecordingAdapterCanInvertCreateTable(): void { $table = new Table('atable', [], $this->adapter); $table->addColumn('column1', 'string') ->save(); $commands = $this->adapter->getInvertedCommands()->getActions(); - $this->assertInstanceOf('Migrations\Db\Action\DropTable', $commands[0]); + $this->assertInstanceOf(DropTable::class, $commands[0]); $this->assertEquals('atable', $commands[0]->getTable()->getName()); } - public function testRecordingAdapterCanInvertRenameTable() + public function testRecordingAdapterCanInvertRenameTable(): void { $table = new Table('oldname', [], $this->adapter); $table->rename('newname') ->save(); $commands = $this->adapter->getInvertedCommands()->getActions(); - $this->assertInstanceOf('Migrations\Db\Action\RenameTable', $commands[0]); + $this->assertInstanceOf(RenameTable::class, $commands[0]); $this->assertEquals('newname', $commands[0]->getTable()->getName()); $this->assertEquals('oldname', $commands[0]->getNewName()); } - public function testRecordingAdapterCanInvertAddColumn() + public function testRecordingAdapterCanInvertAddColumn(): void { $this->adapter ->getAdapter() @@ -82,12 +85,12 @@ public function testRecordingAdapterCanInvertAddColumn() ->save(); $commands = $this->adapter->getInvertedCommands()->getActions(); - $this->assertInstanceOf('Migrations\Db\Action\RemoveColumn', $commands[0]); + $this->assertInstanceOf(RemoveColumn::class, $commands[0]); $this->assertEquals('atable', $commands[0]->getTable()->getName()); $this->assertEquals('acolumn', $commands[0]->getColumn()->getName()); } - public function testRecordingAdapterCanInvertRenameColumn() + public function testRecordingAdapterCanInvertRenameColumn(): void { $this->adapter ->getAdapter() @@ -100,12 +103,12 @@ public function testRecordingAdapterCanInvertRenameColumn() ->save(); $commands = $this->adapter->getInvertedCommands()->getActions(); - $this->assertInstanceOf('Migrations\Db\Action\RenameColumn', $commands[0]); + $this->assertInstanceOf(RenameColumn::class, $commands[0]); $this->assertEquals('newname', $commands[0]->getColumn()->getName()); $this->assertEquals('oldname', $commands[0]->getNewName()); } - public function testRecordingAdapterCanInvertAddIndex() + public function testRecordingAdapterCanInvertAddIndex(): void { $this->adapter ->getAdapter() @@ -118,12 +121,12 @@ public function testRecordingAdapterCanInvertAddIndex() ->save(); $commands = $this->adapter->getInvertedCommands()->getActions(); - $this->assertInstanceOf('Migrations\Db\Action\DropIndex', $commands[0]); + $this->assertInstanceOf(DropIndex::class, $commands[0]); $this->assertEquals('atable', $commands[0]->getTable()->getName()); $this->assertEquals(['email'], $commands[0]->getIndex()->getColumns()); } - public function testRecordingAdapterCanInvertAddForeignKey() + public function testRecordingAdapterCanInvertAddForeignKey(): void { $this->adapter ->getAdapter() @@ -136,12 +139,12 @@ public function testRecordingAdapterCanInvertAddForeignKey() ->save(); $commands = $this->adapter->getInvertedCommands()->getActions(); - $this->assertInstanceOf('Migrations\Db\Action\DropForeignKey', $commands[0]); + $this->assertInstanceOf(DropForeignKey::class, $commands[0]); $this->assertEquals('atable', $commands[0]->getTable()->getName()); $this->assertEquals(['ref_table_id'], $commands[0]->getForeignKey()->getColumns()); } - public function testGetInvertedCommandsThrowsExceptionForIrreversibleCommand() + public function testGetInvertedCommandsThrowsExceptionForIrreversibleCommand(): void { $this->adapter ->getAdapter() diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index 2fd54a9bd..a66031d52 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -27,13 +27,12 @@ class SqliteAdapterTest extends TestCase { private array $config; + private StubConsoleOutput $out; + private ConsoleIo $io; - /** - * @var \Migrations\Db\Adapter\SqliteAdapter - */ - private $adapter; + private SqliteAdapter $adapter; protected function setUp(): void { @@ -78,17 +77,17 @@ protected function tearDown(): void unset($this->adapter, $this->out, $this->io); } - public function testGetConnection() + public function testGetConnection(): void { error_reporting(E_ALL); - $this->deprecated(function () { + $this->deprecated(function (): void { $connection = $this->adapter->getConnection(); $this->assertInstanceOf(Connection::class, $connection); $this->assertSame($connection, $this->adapter->getDecoratedConnection()); }); } - public function testBeginTransaction() + public function testBeginTransaction(): void { $this->adapter->beginTransaction(); @@ -98,7 +97,7 @@ public function testBeginTransaction() ); } - public function testRollbackTransaction() + public function testRollbackTransaction(): void { $this->adapter->beginTransaction(); $this->adapter->rollbackTransaction(); @@ -109,7 +108,7 @@ public function testRollbackTransaction() ); } - public function testCommitTransactionTransaction() + public function testCommitTransactionTransaction(): void { $this->adapter->beginTransaction(); $this->adapter->commitTransaction(); @@ -120,7 +119,7 @@ public function testCommitTransactionTransaction() ); } - public function testCreatingTheSchemaTableOnConnect() + public function testCreatingTheSchemaTableOnConnect(): void { $this->adapter->connect(); $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); @@ -131,24 +130,24 @@ public function testCreatingTheSchemaTableOnConnect() $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); } - public function testSchemaTableIsCreatedWithPrimaryKey() + public function testSchemaTableIsCreatedWithPrimaryKey(): void { $this->adapter->connect(); new Table($this->adapter->getSchemaTableName(), [], $this->adapter); $this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version'])); } - public function testQuoteTableName() + public function testQuoteTableName(): void { $this->assertEquals('"test_table"', $this->adapter->quoteTableName('test_table')); } - public function testQuoteColumnName() + public function testQuoteColumnName(): void { $this->assertEquals('"test_column"', $this->adapter->quoteColumnName('test_column')); } - public function testCreateTable() + public function testCreateTable(): void { $table = new Table('ntable', [], $this->adapter); $table->addColumn('realname', 'string') @@ -161,7 +160,7 @@ public function testCreateTable() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableCustomIdColumn() + public function testCreateTableCustomIdColumn(): void { $table = new Table('ntable', ['id' => 'custom_id'], $this->adapter); $table->addColumn('realname', 'string') @@ -181,7 +180,7 @@ public function testCreateTableCustomIdColumn() $this->assertFalse($idColumn->isNull()); } - public function testCreateTableIdentityIdColumn() + public function testCreateTableIdentityIdColumn(): void { $table = new Table('ntable', ['id' => false, 'primary_key' => ['custom_id']], $this->adapter); $table->addColumn('custom_id', 'integer', ['identity' => true]) @@ -195,7 +194,7 @@ public function testCreateTableIdentityIdColumn() $this->assertTrue($idColumn->getIdentity()); } - public function testCreateTableWithNoPrimaryKey() + public function testCreateTableWithNoPrimaryKey(): void { $options = [ 'id' => false, @@ -206,7 +205,7 @@ public function testCreateTableWithNoPrimaryKey() $this->assertFalse($this->adapter->hasColumn('atable', 'id')); } - public function testCreateTableWithMultiplePrimaryKeys() + public function testCreateTableWithMultiplePrimaryKeys(): void { $options = [ 'id' => false, @@ -226,7 +225,7 @@ public function testCreateTableWithMultiplePrimaryKeys() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsUuid() + public function testCreateTableWithPrimaryKeyAsUuid(): void { $options = [ 'id' => false, @@ -241,7 +240,7 @@ public function testCreateTableWithPrimaryKeyAsUuid() /** * @return void */ - public function testCreateTableWithPrimaryKeyAsBinaryUuid() + public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void { $options = [ 'id' => false, @@ -253,7 +252,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid() $this->assertTrue($this->adapter->hasIndex('ztable', 'id')); } - public function testCreateTableWithMultipleIndexes() + public function testCreateTableWithMultipleIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -267,7 +266,7 @@ public function testCreateTableWithMultipleIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name'])); } - public function testCreateTableWithUniqueIndexes() + public function testCreateTableWithUniqueIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -277,7 +276,7 @@ public function testCreateTableWithUniqueIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email'])); } - public function testCreateTableWithNamedIndexes() + public function testCreateTableWithNamedIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -288,7 +287,7 @@ public function testCreateTableWithNamedIndexes() $this->assertTrue($this->adapter->hasIndexByName('table1', 'myemailindex')); } - public function testCreateTableWithForeignKey() + public function testCreateTableWithForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -302,7 +301,7 @@ public function testCreateTableWithForeignKey() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testCreateTableWithIndexesAndForeignKey() + public function testCreateTableWithIndexesAndForeignKey(): void { $refTable = new Table('tbl_master', [], $this->adapter); $refTable->create(); @@ -335,7 +334,7 @@ public function testCreateTableWithIndexesAndForeignKey() ); } - public function testCreateTableWithoutAutoIncrementingPrimaryKeyAndWithForeignKey() + public function testCreateTableWithoutAutoIncrementingPrimaryKeyAndWithForeignKey(): void { $refTable = (new Table('tbl_master', ['id' => false, 'primary_key' => 'id'], $this->adapter)) ->addColumn('id', 'text'); @@ -384,7 +383,7 @@ public function testCreateTableIndexWithWhere(): void $this->assertStringContainsString('("email" ASC) WHERE is_verified = true', $indexQuery); } - public function testAddPrimaryKey() + public function testAddPrimaryKey(): void { $table = new Table('table1', [], $this->adapter); $table @@ -396,7 +395,7 @@ public function testAddPrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['id'])); } - public function testChangePrimaryKey() + public function testChangePrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -412,7 +411,7 @@ public function testChangePrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2'])); } - public function testChangePrimaryKeyNonInteger() + public function testChangePrimaryKeyNonInteger(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -428,7 +427,7 @@ public function testChangePrimaryKeyNonInteger() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2'])); } - public function testChangePrimaryKeyWithoutAutoIncrement() + public function testChangePrimaryKeyWithoutAutoIncrement(): void { // Create table with id_1 as PK without AUTOINCREMENT keyword $this->adapter->execute('CREATE TABLE table1 (id_1 INTEGER NOT NULL PRIMARY KEY, id_2 INTEGER NOT NULL)'); @@ -450,7 +449,7 @@ public function testChangePrimaryKeyWithoutAutoIncrement() $this->assertStringNotContainsString('AUTOINCREMENT', $result['sql'], 'AUTOINCREMENT should not be added when changing PK to a column that did not have it'); } - public function testChangePrimaryKeyFromAutoIncrementColumn() + public function testChangePrimaryKeyFromAutoIncrementColumn(): void { // Create table with id_1 as PK with AUTOINCREMENT $this->adapter->execute('CREATE TABLE table1 (id_1 INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id_2 INTEGER NOT NULL)'); @@ -473,7 +472,7 @@ public function testChangePrimaryKeyFromAutoIncrementColumn() $this->assertStringNotContainsString('AUTOINCREMENT', $result['sql'], 'AUTOINCREMENT should not be added when changing PK to a column that never had it'); } - public function testDropPrimaryKey() + public function testDropPrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -488,7 +487,7 @@ public function testDropPrimaryKey() $this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testAddMultipleColumnPrimaryKeyFails() + public function testAddMultipleColumnPrimaryKeyFails(): void { $table = new Table('table1', [], $this->adapter); $table @@ -503,7 +502,7 @@ public function testAddMultipleColumnPrimaryKeyFails() ->save(); } - public function testChangeCommentFails() + public function testChangeCommentFails(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -515,7 +514,7 @@ public function testChangeCommentFails() ->save(); } - public function testRenameTable() + public function testRenameTable(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -526,7 +525,7 @@ public function testRenameTable() $this->assertTrue($this->adapter->hasTable('table2')); } - public function testAddColumn() + public function testAddColumn(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -541,7 +540,7 @@ public function testAddColumn() // $this->assertEquals('realname', $rows[1]['Field']); } - public function testAddColumnWithDefaultValue() + public function testAddColumnWithDefaultValue(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -551,7 +550,7 @@ public function testAddColumnWithDefaultValue() $this->assertEquals("'test'", $rows[1]['dflt_value']); } - public function testAddColumnWithDefaultZero() + public function testAddColumnWithDefaultZero(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -562,7 +561,7 @@ public function testAddColumnWithDefaultZero() $this->assertEquals('0', $rows[1]['dflt_value']); } - public function testAddColumnWithDefaultEmptyString() + public function testAddColumnWithDefaultEmptyString(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -572,7 +571,7 @@ public function testAddColumnWithDefaultEmptyString() $this->assertEquals("''", $rows[1]['dflt_value']); } - public function testAddDecimalWithPrecisionAndScale() + public function testAddDecimalWithPrecisionAndScale(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -620,7 +619,7 @@ public function testAddColumnToIrregularCreateTableStatements(string $createTabl } } - public function testRenameColumn() + public function testRenameColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -631,7 +630,7 @@ public function testRenameColumn() $this->assertTrue($this->adapter->hasColumn('t', 'column2')); } - public function testRenamingANonExistentColumn() + public function testRenamingANonExistentColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -642,7 +641,7 @@ public function testRenamingANonExistentColumn() $this->adapter->renameColumn('t', 'column2', 'column1'); } - public function testRenameColumnWithIndex() + public function testRenameColumnWithIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -659,7 +658,7 @@ public function testRenameColumnWithIndex() $this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol')); } - public function testRenameColumnWithUniqueIndex() + public function testRenameColumnWithUniqueIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -676,7 +675,7 @@ public function testRenameColumnWithUniqueIndex() $this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol')); } - public function testRenameColumnWithCompositeIndex() + public function testRenameColumnWithCompositeIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -698,7 +697,7 @@ public function testRenameColumnWithCompositeIndex() * Tests that rewriting the index SQL does not accidentally change * the table name in case it matches the column name. */ - public function testRenameColumnWithIndexMatchingTheTableName() + public function testRenameColumnWithIndexMatchingTheTableName(): void { $table = new Table('indexcol', [], $this->adapter); $table @@ -719,7 +718,7 @@ public function testRenameColumnWithIndexMatchingTheTableName() * Tests that rewriting the index SQL does not accidentally change * column names that partially match the column to rename. */ - public function testRenameColumnWithIndexColumnPartialMatch() + public function testRenameColumnWithIndexColumnPartialMatch(): void { $table = new Table('t', [], $this->adapter); $table @@ -738,7 +737,7 @@ public function testRenameColumnWithIndexColumnPartialMatch() $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcolumn', 'newindexcol'])); } - public function testRenameColumnWithIndexColumnRequiringQuoting() + public function testRenameColumnWithIndexColumnRequiringQuoting(): void { $table = new Table('t', [], $this->adapter); $table @@ -758,7 +757,7 @@ public function testRenameColumnWithIndexColumnRequiringQuoting() /** * Indices that are using expressions are not being updated. */ - public function testRenameColumnWithExpressionIndex() + public function testRenameColumnWithExpressionIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -847,7 +846,7 @@ public static function customIndexSQLDataProvider(): array * @param string $newIndexSQL Expected new index creation SQL */ #[DataProvider('customIndexSQLDataProvider')] - public function testRenameColumnWithCustomIndex(string $indexSQL, string $newIndexSQL) + public function testRenameColumnWithCustomIndex(string $indexSQL, string $newIndexSQL): void { $table = new Table('t', [], $this->adapter); $table @@ -946,7 +945,7 @@ public static function customCompositeIndexSQLDataProvider(): array * @param string $newIndexSQL Expected new index creation SQL */ #[DataProvider('customCompositeIndexSQLDataProvider')] - public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL) + public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL): void { $table = new Table('t', [], $this->adapter); $table @@ -969,7 +968,7 @@ public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, strin $this->assertSame($index['sql'], $newIndexSQL); } - public function testChangeColumn() + public function testChangeColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -978,6 +977,7 @@ public function testChangeColumn() $newColumn1 = new Column(); $newColumn1->setName('column1'); $newColumn1->setType('string'); + $table->changeColumn('column1', $newColumn1); $this->assertTrue($this->adapter->hasColumn('t', 'column1')); $newColumn2 = new Column(); @@ -988,7 +988,7 @@ public function testChangeColumn() $this->assertTrue($this->adapter->hasColumn('t', 'column2')); } - public function testChangeColumnDefaultValue() + public function testChangeColumnDefaultValue(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test']) @@ -1004,7 +1004,7 @@ public function testChangeColumnDefaultValue() $this->assertEquals("'test1'", $rows[1]['dflt_value']); } - public function testChangeColumnWithForeignKey() + public function testChangeColumnWithForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1022,7 +1022,7 @@ public function testChangeColumnWithForeignKey() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testChangeColumnWithIndex() + public function testChangeColumnWithIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -1040,7 +1040,7 @@ public function testChangeColumnWithIndex() $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); } - public function testChangeColumnWithTrigger() + public function testChangeColumnWithTrigger(): void { $table = new Table('t', [], $this->adapter); $table @@ -1075,7 +1075,7 @@ public function testChangeColumnWithTrigger() $this->assertEquals($triggerSQL, $rows[0]['sql']); } - public function testChangeColumnDefaultToZero() + public function testChangeColumnDefaultToZero(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'integer') @@ -1089,7 +1089,7 @@ public function testChangeColumnDefaultToZero() $this->assertEquals('0', $rows[1]['dflt_value']); } - public function testChangeColumnDefaultToNull() + public function testChangeColumnDefaultToNull(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test']) @@ -1103,7 +1103,7 @@ public function testChangeColumnDefaultToNull() $this->assertNull($rows[1]['dflt_value']); } - public function testChangeColumnWithCommasInCommentsOrDefaultValue() + public function testChangeColumnWithCommasInCommentsOrDefaultValue(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'one, two or three', 'comment' => 'three, two or one']) @@ -1119,11 +1119,11 @@ public function testChangeColumnWithCommasInCommentsOrDefaultValue() } #[DataProvider('columnCreationArgumentProvider')] - public function testDropColumn($columnCreationArgs) + public function testDropColumn(array $columnCreationArgs): void { $table = new Table('t', [], $this->adapter); $columnName = $columnCreationArgs[0]; - call_user_func_array([$table, 'addColumn'], $columnCreationArgs); + $table->addColumn(...$columnCreationArgs); $table->save(); $this->assertTrue($this->adapter->hasColumn('t', $columnName)); @@ -1132,7 +1132,7 @@ public function testDropColumn($columnCreationArgs) $this->assertFalse($this->adapter->hasColumn('t', $columnName)); } - public function testDropColumnWithIndex() + public function testDropColumnWithIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -1147,7 +1147,7 @@ public function testDropColumnWithIndex() $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); } - public function testDropColumnWithUniqueIndex() + public function testDropColumnWithUniqueIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -1162,7 +1162,7 @@ public function testDropColumnWithUniqueIndex() $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); } - public function testDropColumnWithCompositeIndex() + public function testDropColumnWithCompositeIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -1182,7 +1182,7 @@ public function testDropColumnWithCompositeIndex() * Tests that removing columns does not accidentally drop indices * on table names that match the column to remove. */ - public function testDropColumnWithIndexMatchingTheTableName() + public function testDropColumnWithIndexMatchingTheTableName(): void { $table = new Table('indexcol', [], $this->adapter); $table @@ -1202,7 +1202,7 @@ public function testDropColumnWithIndexMatchingTheTableName() * Tests that removing columns does not accidentally drop indices * that contain column names that partially match the column to remove. */ - public function testDropColumnWithIndexColumnPartialMatch() + public function testDropColumnWithIndexColumnPartialMatch(): void { $table = new Table('t', [], $this->adapter); $table @@ -1222,7 +1222,7 @@ public function testDropColumnWithIndexColumnPartialMatch() /** * Indices with expressions are not being removed. */ - public function testDropColumnWithExpressionIndex() + public function testDropColumnWithExpressionIndex(): void { $table = new Table('t', [], $this->adapter); $table @@ -1244,7 +1244,7 @@ public function testDropColumnWithExpressionIndex() * @param string $newIndexSQL Expected new index creation SQL */ #[DataProvider('customIndexSQLDataProvider')] - public function testDropColumnWithCustomIndex(string $indexSQL, string $newIndexSQL) + public function testDropColumnWithCustomIndex(string $indexSQL, string $newIndexSQL): void { $table = new Table('t', [], $this->adapter); $table @@ -1265,7 +1265,7 @@ public function testDropColumnWithCustomIndex(string $indexSQL, string $newIndex * @param string $newIndexSQL Expected new index creation SQL */ #[DataProvider('customCompositeIndexSQLDataProvider')] - public function testDropColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL) + public function testDropColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL): void { $table = new Table('t', [], $this->adapter); $table @@ -1285,7 +1285,7 @@ public function testDropColumnWithCustomCompositeIndex(string $indexSQL, string $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol3'])); } - public static function columnCreationArgumentProvider() + public static function columnCreationArgumentProvider(): array { return [ [['column1', 'string']], @@ -1293,7 +1293,7 @@ public static function columnCreationArgumentProvider() ]; } - public static function columnsProvider() + public static function columnsProvider(): array { return [ ['column1', 'string', []], @@ -1313,7 +1313,7 @@ public static function columnsProvider() ]; } - public function testAddIndex() + public function testAddIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1324,7 +1324,7 @@ public function testAddIndex() $this->assertTrue($table->hasIndex('email')); } - public function testDropIndex() + public function testDropIndex(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -1365,7 +1365,7 @@ public function testDropIndex() $this->assertFalse($table4->hasIndex(['fname', 'lname'])); } - public function testDropIndexByName() + public function testDropIndexByName(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -1387,7 +1387,7 @@ public function testDropIndexByName() $this->assertFalse($table2->hasIndex(['fname', 'lname'])); } - public function testAddForeignKey() + public function testAddForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -1401,7 +1401,7 @@ public function testAddForeignKey() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKey() + public function testDropForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string') @@ -1431,7 +1431,7 @@ public function testDropForeignKey() $this->assertTrue($this->adapter->hasTable($table->getName())); } - public function testDropForeignKeyWithQuoteVariants() + public function testDropForeignKeyWithQuoteVariants(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string') @@ -1465,7 +1465,7 @@ public function testDropForeignKeyWithQuoteVariants() $this->assertFalse($this->adapter->hasForeignKey('table', ['ref_lots_of_space', 'ref_lots_of_space'])); } - public function testDropForeignKeyWithMultipleColumns() + public function testDropForeignKeyWithMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1525,11 +1525,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array ]; } - /** - * @param array $columns - */ #[DataProvider('nonExistentForeignKeyColumnsProvider')] - public function testDropForeignKeyByNonExistentKeyColumns(array $columns) + public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1557,7 +1554,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns) $this->adapter->dropForeignKey($table->getName(), $columns); } - public function testDropForeignKeyCaseInsensitivity() + public function testDropForeignKeyCaseInsensitivity(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1572,7 +1569,7 @@ public function testDropForeignKeyCaseInsensitivity() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyByName() + public function testDropForeignKeyByName(): void { $this->expectExceptionMessage('SQLite does not have named foreign keys'); $this->expectException(BadMethodCallException::class); @@ -1594,7 +1591,7 @@ public function testDropForeignKeyByName() $this->adapter->dropForeignKey($table->getName(), [], 'my_constraint'); } - public function testHasDatabase() + public function testHasDatabase(): void { if ($this->config['database'] === ':memory:') { $this->markTestSkipped('Skipping hasDatabase() when testing in-memory db.'); @@ -1603,7 +1600,7 @@ public function testHasDatabase() $this->assertTrue($this->adapter->hasDatabase($this->config['database'])); } - public function testDropDatabase() + public function testDropDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('phinx_temp_database')); $this->adapter->createDatabase('phinx_temp_database'); @@ -1611,7 +1608,7 @@ public function testDropDatabase() $this->adapter->dropDatabase('phinx_temp_database'); } - public function testAddColumnWithComment() + public function testAddColumnWithComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string', ['comment' => 'Comments from "column1"']) @@ -1628,7 +1625,7 @@ public function testAddColumnWithComment() $this->assertMatchesRegularExpression('/\/\* Comments from "column1" \*\//', $sql); } - public function testAddColumnTableWithConstraint() + public function testAddColumnTableWithConstraint(): void { $this->adapter->execute('PRAGMA foreign_keys = ON'); $roles = new Table('constraint_roles', [], $this->adapter); @@ -1648,13 +1645,13 @@ public function testAddColumnTableWithConstraint() $updatedRoles ->addColumn('description', 'string', ['default' => 'short desc']) ->update(); - $res = $this->adapter->fetchAll('select * from sqlite_master where type = \'table\''); + $res = $this->adapter->fetchAll("select * from sqlite_master where type = 'table'"); $res = $this->adapter->fetchRow('select * from constraint_roles LIMIT 1'); $this->assertArrayHasKey('description', $res, 'Should have new column in output'); $this->assertEquals('short desc', $res['description']); } - public function testAddIndexTwoTablesSameIndex() + public function testAddIndexTwoTablesSameIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -1675,7 +1672,7 @@ public function testAddIndexTwoTablesSameIndex() $this->assertTrue($table2->hasIndex('email')); } - public function testBulkInsertData() + public function testBulkInsertData(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1698,7 +1695,7 @@ public function testBulkInsertData() ) ->insert( [ - 'column1' => '\'value4\'', + 'column1' => "'value4'", 'column2' => null, ], ) @@ -1708,14 +1705,14 @@ public function testBulkInsertData() $this->assertEquals('value1', $rows[0]['column1']); $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); - $this->assertEquals('\'value4\'', $rows[3]['column1']); + $this->assertEquals("'value4'", $rows[3]['column1']); $this->assertEquals(1, $rows[0]['column2']); $this->assertEquals(2, $rows[1]['column2']); $this->assertEquals(3, $rows[2]['column2']); $this->assertNull($rows[3]['column2']); } - public function testBulkInsertLiteral() + public function testBulkInsertLiteral(): void { $data = [ [ @@ -1741,12 +1738,12 @@ public function testBulkInsertLiteral() $this->assertEquals('value1', $rows[0]['column1']); $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']); $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column2']); $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column2']); } - public function testInsertData() + public function testInsertData(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1769,7 +1766,7 @@ public function testInsertData() ) ->insert( [ - 'column1' => '\'value4\'', + 'column1' => "'value4'", 'column2' => null, ], ) @@ -1780,14 +1777,14 @@ public function testInsertData() $this->assertEquals('value1', $rows[0]['column1']); $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); - $this->assertEquals('\'value4\'', $rows[3]['column1']); + $this->assertEquals("'value4'", $rows[3]['column1']); $this->assertEquals(1, $rows[0]['column2']); $this->assertEquals(2, $rows[1]['column2']); $this->assertEquals(3, $rows[2]['column2']); $this->assertNull($rows[3]['column2']); } - public function testInsertLiteral() + public function testInsertLiteral(): void { $data = [ [ @@ -1818,12 +1815,12 @@ public function testInsertLiteral() $this->assertEquals('test', $rows[0]['column2']); $this->assertEquals('test', $rows[1]['column2']); $this->assertEquals('foo', $rows[2]['column2']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']); $this->assertEquals('2024-01-01 00:00:00', $rows[1]['column3']); $this->assertEquals('2025-01-01 00:00:00', $rows[2]['column3']); } - public function testBulkInsertDataEnum() + public function testBulkInsertDataEnum(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1841,7 +1838,7 @@ public function testBulkInsertDataEnum() $this->assertEquals('c', $rows[0]['column3']); } - public function testNullWithoutDefaultValue() + public function testNullWithoutDefaultValue(): void { // construct table with default/null combinations $table = new Table('table1', [], $this->adapter); @@ -1878,7 +1875,7 @@ public function testNullWithoutDefaultValue() $this->assertEquals('some2', $dd->getDefault()); } - public function testDumpCreateTable() + public function testDumpCreateTable(): void { $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $table = new Table('table1', [], $this->adapter); @@ -1891,7 +1888,7 @@ public function testDumpCreateTable() $expectedOutput = <<<'OUTPUT' CREATE TABLE "table1" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "column1" VARCHAR NOT NULL, "column2" INTEGER, "column3" VARCHAR DEFAULT 'test'); OUTPUT; - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create table query to the output'); } @@ -1900,7 +1897,7 @@ public function testDumpCreateTable() * Then sets phinx to dry run mode and inserts a record. * Asserts that phinx outputs the insert statement and doesn't insert a record. */ - public function testDumpInsert() + public function testDumpInsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -1925,9 +1922,9 @@ public function testDumpInsert() INSERT INTO "table1" ("string_col") VALUES (null); INSERT INTO "table1" ("int_col") VALUES (23); OUTPUT; - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); $actualOutput = preg_replace("/\r\n|\r/", "\n", $actualOutput); // normalize line endings for Windows - $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option doesn\'t dump the insert to the output'); + $this->assertStringContainsString($expectedOutput, $actualOutput, "Passing the --dry-run option doesn't dump the insert to the output"); $countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1'); $this->assertTrue($countQuery->execute()); @@ -1940,7 +1937,7 @@ public function testDumpInsert() * Then sets phinx to dry run mode and inserts some records. * Asserts that phinx outputs the insert statement and doesn't insert any record. */ - public function testDumpBulkinsert() + public function testDumpBulkinsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -1962,8 +1959,8 @@ public function testDumpBulkinsert() $expectedOutput = <<<'OUTPUT' INSERT INTO "table1" ("string_col", "int_col") VALUES ('test_data1', 23), (null, 42); OUTPUT; - $actualOutput = join("\n", $this->out->messages()); - $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option doesn\'t dump the bulkinsert to the output'); + $actualOutput = implode("\n", $this->out->messages()); + $this->assertStringContainsString($expectedOutput, $actualOutput, "Passing the --dry-run option doesn't dump the bulkinsert to the output"); $countQuery = $this->adapter->query('SELECT COUNT(*) FROM table1'); $this->assertTrue($countQuery->execute()); @@ -1971,7 +1968,7 @@ public function testDumpBulkinsert() $this->assertEquals(0, $res[0][0]); } - public function testDumpCreateTableAndThenInsert() + public function testDumpCreateTableAndThenInsert(): void { $this->adapter->setOptions($this->adapter->getOptions() + ['dryrun' => true]); $table = new Table('table1', ['id' => false, 'primary_key' => ['column1']], $this->adapter); @@ -1992,7 +1989,7 @@ public function testDumpCreateTableAndThenInsert() CREATE TABLE "table1" ("column1" VARCHAR NOT NULL, "column2" INTEGER, PRIMARY KEY ("column1")); INSERT INTO "table1" ("column1", "column2") VALUES ('id1', 1); OUTPUT; - $actualOutput = join("\n", $this->out->messages()); + $actualOutput = implode("\n", $this->out->messages()); $actualOutput = preg_replace("/\r\n|\r/", "\n", $actualOutput); // normalize line endings for Windows $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create and then insert table queries to the output'); } @@ -2000,7 +1997,7 @@ public function testDumpCreateTableAndThenInsert() /** * Tests interaction with the query builder */ - public function testQueryBuilder() + public function testQueryBuilder(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2039,7 +2036,7 @@ public function testQueryBuilder() $this->assertEquals(1, $stm->rowCount()); } - public function testQueryWithParams() + public function testQueryWithParams(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -2074,7 +2071,7 @@ public function testQueryWithParams() * Tests adding more than one column to a table * that already exists due to adapters having different add column instructions */ - public function testAlterTableColumnAdd() + public function testAlterTableColumnAdd(): void { $table = new Table('table1', [], $this->adapter); $table->create(); @@ -2099,14 +2096,14 @@ public function testAlterTableColumnAdd() $columnCount = count($columns); for ($i = 0; $i < $columnCount; $i++) { - $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), "Wrong name for {$expected[$i]['name']}"); - $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), "Wrong type for {$expected[$i]['name']}"); - $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), "Wrong default for {$expected[$i]['name']}"); - $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), "Wrong null for {$expected[$i]['name']}"); + $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), 'Wrong name for ' . $expected[$i]['name']); + $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), 'Wrong type for ' . $expected[$i]['name']); + $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), 'Wrong default for ' . $expected[$i]['name']); + $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), 'Wrong null for ' . $expected[$i]['name']); } } - public function testAlterTableWithConstraints() + public function testAlterTableWithConstraints(): void { $table = new Table('table1', [], $this->adapter); $table->create(); @@ -2135,10 +2132,10 @@ public function testAlterTableWithConstraints() $columnCount = count($columns); for ($i = 0; $i < $columnCount; $i++) { - $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), "Wrong name for {$expected[$i]['name']}"); - $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), "Wrong type for {$expected[$i]['name']}"); - $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), "Wrong default for {$expected[$i]['name']}"); - $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), "Wrong null for {$expected[$i]['name']}"); + $this->assertSame($expected[$i]['name'], $columns[$i]->getName(), 'Wrong name for ' . $expected[$i]['name']); + $this->assertSame($expected[$i]['type'], $columns[$i]->getType(), 'Wrong type for ' . $expected[$i]['name']); + $this->assertSame($expected[$i]['default'], $columns[$i]->getDefault() instanceof Literal ? (string)$columns[$i]->getDefault() : $columns[$i]->getDefault(), 'Wrong default for ' . $expected[$i]['name']); + $this->assertSame($expected[$i]['null'], $columns[$i]->getNull(), 'Wrong null for ' . $expected[$i]['name']); } } @@ -2146,7 +2143,7 @@ public function testAlterTableWithConstraints() * Tests that operations that trigger implicit table drops will not cause * a foreign key constraint violation error. */ - public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint() + public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint(): void { $this->adapter->execute('PRAGMA foreign_keys = ON'); @@ -2213,7 +2210,7 @@ public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint() * alteration process (being it implicitly by the process itself or by the user) * will trigger an error accordingly. */ - public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange() + public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange(): void { $articlesTable = new Table('articles', [], $this->adapter); $articlesTable @@ -2249,7 +2246,7 @@ public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange * alteration process (being it implicitly by the process itself or by the user) * will trigger an error accordingly. */ - public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange() + public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange(): void { $adapter = $this ->getMockBuilder(SqliteAdapter::class) @@ -2302,7 +2299,7 @@ public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange * Tests that the adapter's foreign key validation does not apply when * the `foreign_keys` pragma is set to `OFF`. */ - public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabledForeignKeys() + public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabledForeignKeys(): void { $articlesTable = new Table('articles', [], $this->adapter); $articlesTable @@ -2345,7 +2342,7 @@ public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabl ->update(); } - public function testLiteralSupport() + public function testLiteralSupport(): void { $createQuery = <<<'INPUT' CREATE TABLE `test` (`real_col` DECIMAL) @@ -2358,23 +2355,23 @@ public function testLiteralSupport() } #[DataProvider('provideTableNamesForPresenceCheck')] - public function testHasTable($createName, $tableName, $exp) + public function testHasTable(string $createName, string $tableName, bool $exp): void { // Test case for issue #1535 $conn = $this->adapter->getConnection(); - $conn->execute('ATTACH DATABASE \':memory:\' as etc'); + $conn->execute("ATTACH DATABASE ':memory:' as etc"); $conn->execute('ATTACH DATABASE \':memory:\' as "main.db"'); $conn->execute(sprintf('DROP TABLE IF EXISTS %s', $createName)); $this->assertFalse($this->adapter->hasTable($tableName), sprintf('Adapter claims table %s exists when it does not', $tableName)); $conn->execute(sprintf('CREATE TABLE %s (a text)', $createName)); - if ($exp == true) { + if ($exp) { $this->assertTrue($this->adapter->hasTable($tableName), sprintf('Adapter claims table %s does not exist when it does', $tableName)); } else { $this->assertFalse($this->adapter->hasTable($tableName), sprintf('Adapter claims table %s exists when it does not', $tableName)); } } - public static function provideTableNamesForPresenceCheck() + public static function provideTableNamesForPresenceCheck(): array { return [ 'Ordinary table' => ['t', 't', true], @@ -2386,8 +2383,8 @@ public static function provideTableNamesForPresenceCheck() 'Wrong schema 1' => ['t', 'etc.t', false], 'Wrong schema 2' => ['t', 'temp.t', false], 'Missing schema' => ['t', 'not_attached.t', false], - 'Malicious table' => ['"\'"', '\'', true], - 'Malicious missing table' => ['t', '\'', false], + 'Malicious table' => ['"\'"', "'", true], + 'Malicious missing table' => ['t', "'", false], 'Table name case 1' => ['t', 'T', true], 'Table name case 2' => ['T', 't', true], 'Schema name case 1' => ['main.t', 'MAIN.t', true], @@ -2424,10 +2421,10 @@ public function testHasTableAfterExecuteDrop(): void } #[DataProvider('provideIndexColumnsToCheck')] - public function testHasIndex($tableDef, $cols, $exp) + public function testHasIndex(string $tableDef, string|array $cols, bool $exp): void { $conn = $this->adapter->getConnection(); - if (strpos($tableDef, ';') !== false) { + if (str_contains($tableDef, ';')) { $queries = explode(';', $tableDef); foreach ($queries as $query) { $stmt = $conn->execute($query); @@ -2441,7 +2438,7 @@ public function testHasIndex($tableDef, $cols, $exp) $this->assertEquals($exp, $this->adapter->hasIndex('t', $cols)); } - public static function provideIndexColumnsToCheck() + public static function provideIndexColumnsToCheck(): array { return [ ['create table t(a text)', 'a', false], @@ -2464,10 +2461,10 @@ public static function provideIndexColumnsToCheck() } #[DataProvider('provideIndexNamesToCheck')] - public function testHasIndexByName($tableDef, $index, $exp) + public function testHasIndexByName(string $tableDef, string $index, bool $exp): void { $conn = $this->adapter->getConnection(); - if (strpos($tableDef, ';') !== false) { + if (str_contains($tableDef, ';')) { $queries = explode(';', $tableDef); foreach ($queries as $query) { $stmt = $conn->execute($query); @@ -2480,7 +2477,7 @@ public function testHasIndexByName($tableDef, $index, $exp) $this->assertEquals($exp, $this->adapter->hasIndexByName('t', $index)); } - public static function provideIndexNamesToCheck() + public static function provideIndexNamesToCheck(): array { return [ ['create table t(a text)', 'test', false], @@ -2496,11 +2493,11 @@ public static function provideIndexNamesToCheck() } #[DataProvider('providePrimaryKeysToCheck')] - public function testHasPrimaryKey($tableDef, $key, $exp) + public function testHasPrimaryKey(string $tableDef, string|array $key, bool $exp): void { $this->assertFalse($this->adapter->hasTable('t'), 'Dirty test fixture'); $conn = $this->adapter->getConnection(); - if (strpos($tableDef, ';') !== false) { + if (str_contains($tableDef, ';')) { $queries = explode(';', $tableDef); foreach ($queries as $query) { $stmt = $conn->execute($query); @@ -2513,7 +2510,7 @@ public function testHasPrimaryKey($tableDef, $key, $exp) $this->assertSame($exp, $this->adapter->hasPrimaryKey('t', $key)); } - public static function providePrimaryKeysToCheck() + public static function providePrimaryKeysToCheck(): array { return [ ['create table t(a integer)', 'a', false], @@ -2525,7 +2522,7 @@ public static function providePrimaryKeysToCheck() ['create table t("a" integer PRIMARY KEY)', 'a', true], ['create table t([a] integer PRIMARY KEY)', 'a', true], ['create table t(`a` integer PRIMARY KEY)', 'a', true], - ['create table t(\'a\' integer PRIMARY KEY)', 'a', true], + ["create table t('a' integer PRIMARY KEY)", 'a', true], ['create table t(`a.a` integer PRIMARY KEY)', 'a.a', true], ['create table t(a integer primary key)', ['a'], true], ['create table t(a integer primary key)', ['a', 'b'], false], @@ -2555,7 +2552,7 @@ public static function providePrimaryKeysToCheck() ]; } - public function testHasNamedPrimaryKey() + public function testHasNamedPrimaryKey(): void { $this->expectException(InvalidArgumentException::class); @@ -2563,11 +2560,11 @@ public function testHasNamedPrimaryKey() } #[DataProvider('provideForeignKeysToCheck')] - public function testHasForeignKey($tableDef, $key, $exp) + public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void { $conn = $this->adapter->getConnection(); $conn->execute('CREATE TABLE other(a integer, b integer, c integer)'); - if (strpos($tableDef, ';') !== false) { + if (str_contains($tableDef, ';')) { $queries = explode(';', $tableDef); foreach ($queries as $query) { $stmt = $conn->execute($query); @@ -2581,7 +2578,7 @@ public function testHasForeignKey($tableDef, $key, $exp) $this->assertSame($exp, $this->adapter->hasForeignKey('t', $key)); } - public static function provideForeignKeysToCheck() + public static function provideForeignKeysToCheck(): array { return [ ['create table t(a integer)', 'a', false], @@ -2723,13 +2720,13 @@ public static function hasNamedForeignKeyProvider(): array } #[DataProvider('provideColumnTypesForValidation')] - public function testIsValidColumnType($phinxType, $exp) + public function testIsValidColumnType(string $phinxType, bool $exp): void { $col = (new Column())->setType($phinxType); $this->assertSame($exp, $this->adapter->isValidColumnType($col)); } - public static function provideColumnTypesForValidation() + public static function provideColumnTypesForValidation(): array { return [ [SqliteAdapter::TYPE_BIGINTEGER, true], @@ -2761,12 +2758,12 @@ public static function provideColumnTypesForValidation() } #[DataProvider('provideDatabaseVersionStrings')] - public function testDatabaseVersionAtLeast($ver, $exp) + public function testDatabaseVersionAtLeast(string $ver, bool $exp): void { $this->assertSame($exp, $this->adapter->databaseVersionAtLeast($ver)); } - public static function provideDatabaseVersionStrings() + public static function provideDatabaseVersionStrings(): array { return [ ['2', true], @@ -2780,10 +2777,10 @@ public static function provideDatabaseVersionStrings() } #[DataProvider('provideColumnNamesToCheck')] - public function testHasColumn($tableDef, $col, $exp) + public function testHasColumn(string $tableDef, string $col, bool $exp): void { $conn = $this->adapter->getConnection(); - if (strpos($tableDef, ';') !== false) { + if (str_contains($tableDef, ';')) { $queries = explode(';', $tableDef); foreach ($queries as $query) { $stmt = $conn->execute($query); @@ -2797,7 +2794,7 @@ public function testHasColumn($tableDef, $col, $exp) $this->assertEquals($exp, $this->adapter->hasColumn('t', $col)); } - public static function provideColumnNamesToCheck() + public static function provideColumnNamesToCheck(): array { return [ ['create table t(a text)', 'a', true], @@ -2805,7 +2802,7 @@ public static function provideColumnNamesToCheck() ['create table t(A text)', 'A', true], ['create table t("a" text)', 'a', true], ['create table t([a] text)', 'a', true], - ['create table t(\'a\' text)', 'a', true], + ["create table t('a' text)", 'a', true], ['create table t("A" text)', 'A', true], ['create table t(a text)', 'a', true], ['create table t(b text)', 'a', false], @@ -2818,10 +2815,11 @@ public static function provideColumnNamesToCheck() ]; } - public function testGetColumns() + public function testGetColumns(): void { $conn = $this->adapter->getConnection(); $conn->execute('create table t(a integer, b text, c char(5), d integer(12,6), e integer not null, f integer null)'); + $exp = [ ['name' => 'a', 'type' => 'integer', 'null' => true, 'limit' => null, 'precision' => null, 'scale' => null], ['name' => 'b', 'type' => 'text', 'null' => true, 'limit' => null, 'precision' => null, 'scale' => null], @@ -2836,16 +2834,17 @@ public function testGetColumns() $this->assertInstanceOf(Column::class, $act[$index]); foreach ($data as $key => $value) { $m = 'get' . ucfirst($key); - $this->assertEquals($value, $act[$index]->$m(), "Parameter '$key' of column at index $index did not match expectations."); + $this->assertEquals($value, $act[$index]->$m(), sprintf("Parameter '%s' of column at index %s did not match expectations.", $key, $index)); } } } #[DataProvider('provideIdentityCandidates')] - public function testGetColumnsForIdentity($tableDef, $exp) + public function testGetColumnsForIdentity(string $tableDef, ?string $exp): void { $conn = $this->adapter->getConnection(); $conn->execute($tableDef); + $cols = $this->adapter->getColumns('t'); $act = []; foreach ($cols as $col) { @@ -2856,7 +2855,7 @@ public function testGetColumnsForIdentity($tableDef, $exp) $this->assertEquals((array)$exp, $act); } - public static function provideIdentityCandidates() + public static function provideIdentityCandidates(): array { return [ ['create table t(a text)', null], @@ -2871,7 +2870,7 @@ public static function provideIdentityCandidates() } #[DataProvider('provideDefaultValues')] - public function testGetColumnsForDefaults($tableDef, $exp) + public function testGetColumnsForDefaults(string $tableDef, string|Literal|int|float|Expression|null $exp): void { $conn = $this->adapter->getConnection(); $conn->execute($tableDef); @@ -2884,7 +2883,7 @@ public function testGetColumnsForDefaults($tableDef, $exp) } } - public static function provideDefaultValues() + public static function provideDefaultValues(): array { return [ 'Implicit null' => ['create table t(a integer)', null], @@ -2905,12 +2904,12 @@ public static function provideDefaultValues() 'Current timestamp LC' => ['create table t(a datetime default current_timestamp)', 'CURRENT_TIMESTAMP'], 'Current timestamp UC' => ['create table t(a datetime default CURRENT_TIMESTAMP)', 'CURRENT_TIMESTAMP'], 'Current timestamp MC' => ['create table t(a datetime default CURRENT_timestamp)', 'CURRENT_TIMESTAMP'], - 'String 1' => ['create table t(a text default \'\')', Literal::from('')], - 'String 2' => ['create table t(a text default \'value!\')', Literal::from('value!')], - 'String 3' => ['create table t(a text default \'O\'\'Brien\')', Literal::from('O\'Brien')], - 'String 4' => ['create table t(a text default \'CURRENT_TIMESTAMP\')', Literal::from('CURRENT_TIMESTAMP')], - 'String 5' => ['create table t(a text default \'current_timestamp\')', Literal::from('current_timestamp')], - 'String 6' => ['create table t(a text default \'\' /* comment */)', Literal::from('')], + 'String 1' => ["create table t(a text default '')", Literal::from('')], + 'String 2' => ["create table t(a text default 'value!')", Literal::from('value!')], + 'String 3' => ["create table t(a text default 'O''Brien')", Literal::from("O'Brien")], + 'String 4' => ["create table t(a text default 'CURRENT_TIMESTAMP')", Literal::from('CURRENT_TIMESTAMP')], + 'String 5' => ["create table t(a text default 'current_timestamp')", Literal::from('current_timestamp')], + 'String 6' => ["create table t(a text default '' /* comment */)", Literal::from('')], 'Hexadecimal LC' => ['create table t(a integer default 0xff)', 255], 'Hexadecimal UC' => ['create table t(a integer default 0XFF)', 255], 'Hexadecimal MC' => ['create table t(a integer default 0x1F)', 31], @@ -2934,26 +2933,27 @@ public static function provideDefaultValues() 'Float 9' => ['create table t(a float default 1e+1)', 10.0], 'Float 10' => ['create table t(a float default 1e-1)', 0.1], 'Float 11' => ['create table t(a float default 1E-1)', 0.1], - 'Blob literal 1' => ['create table t(a float default x\'ff\')', Expression::from('x\'ff\'')], - 'Blob literal 2' => ['create table t(a float default X\'FF\')', Expression::from('X\'FF\'')], + 'Blob literal 1' => ["create table t(a float default x'ff')", Expression::from("x'ff'")], + 'Blob literal 2' => ["create table t(a float default X'FF')", Expression::from("X'FF'")], 'Arbitrary expression' => ['create table t(a float default ((2) + (2)))', Expression::from('(2) + (2)')], - 'Pathological case 1' => ['create table t(a float default (\'/*\' || \'*/\'))', Expression::from('/*\' || \'*/')], + 'Pathological case 1' => ["create table t(a float default ('/*' || '*/'))", Expression::from("/*' || '*/")], ]; } #[DataProvider('provideBooleanDefaultValues')] - public function testGetColumnsForBooleanDefaults($tableDef, $exp) + public function testGetColumnsForBooleanDefaults(string $tableDef, int $exp): void { if (!$this->adapter->databaseVersionAtLeast('3.24')) { $this->markTestSkipped('SQLite 3.24.0 or later is required for this test.'); } $conn = $this->adapter->getConnection(); $conn->execute($tableDef); + $act = $this->adapter->getColumns('t')[0]->getDefault(); $this->assertSame($exp, $act); } - public static function provideBooleanDefaultValues() + public static function provideBooleanDefaultValues(): array { return [ 'True LC' => ['create table t(a boolean default true)', 1], @@ -2966,26 +2966,26 @@ public static function provideBooleanDefaultValues() } #[DataProvider('provideTablesForTruncation')] - public function testTruncateTable($tableDef, $tableName, $tableId) + public function testTruncateTable(string $tableDef, string $tableName, string $tableId): void { $conn = $this->adapter->getConnection(); $conn->execute($tableDef); - $conn->execute("INSERT INTO $tableId default values"); - $conn->execute("INSERT INTO $tableId default values"); - $conn->execute("INSERT INTO $tableId default values"); - $this->assertEquals(3, $conn->execute("select count(*) from $tableId")->fetchColumn(0), 'Broken fixture: data were not inserted properly'); - $this->assertEquals(3, $conn->execute("select max(id) from $tableId")->fetchColumn(0), 'Broken fixture: data were not inserted properly'); + $conn->execute(sprintf('INSERT INTO %s default values', $tableId)); + $conn->execute(sprintf('INSERT INTO %s default values', $tableId)); + $conn->execute(sprintf('INSERT INTO %s default values', $tableId)); + $this->assertEquals(3, $conn->execute('select count(*) from ' . $tableId)->fetchColumn(0), 'Broken fixture: data were not inserted properly'); + $this->assertEquals(3, $conn->execute('select max(id) from ' . $tableId)->fetchColumn(0), 'Broken fixture: data were not inserted properly'); $this->adapter->truncateTable($tableName); - $this->assertEquals(0, $conn->execute("select count(*) from $tableId")->fetchColumn(0), 'Table was not truncated'); - $conn->execute("INSERT INTO $tableId default values"); - $this->assertEquals(1, $conn->execute("select max(id) from $tableId")->fetchColumn(0), 'Autoincrement was not reset'); - $conn->execute("DROP TABLE $tableId"); + $this->assertEquals(0, $conn->execute('select count(*) from ' . $tableId)->fetchColumn(0), 'Table was not truncated'); + $conn->execute(sprintf('INSERT INTO %s default values', $tableId)); + $this->assertEquals(1, $conn->execute('select max(id) from ' . $tableId)->fetchColumn(0), 'Autoincrement was not reset'); + $conn->execute('DROP TABLE ' . $tableId); } /** * @return array */ - public static function provideTablesForTruncation() + public static function provideTablesForTruncation(): array { return [ ['create table t(id integer primary key)', 't', 't'], @@ -3003,7 +3003,7 @@ public static function provideTablesForTruncation() ]; } - public function testForeignKeyReferenceCorrectAfterRenameColumn() + public function testForeignKeyReferenceCorrectAfterRenameColumn(): void { $refTableColumnId = 'ref_table_id'; $refTableColumnToRename = 'columnToRename'; @@ -3019,7 +3019,7 @@ public function testForeignKeyReferenceCorrectAfterRenameColumn() $refTable->renameColumn($refTableColumnToRename, $refTableRenamedColumn)->save(); $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId])); - $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}")); + $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName())); $this->assertTrue($this->adapter->hasColumn($refTable->getName(), $refTableRenamedColumn)); $rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\''); @@ -3028,10 +3028,10 @@ public function testForeignKeyReferenceCorrectAfterRenameColumn() $sql = $row['sql']; } } - $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql); + $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql); } - public function testForeignKeyReferenceCorrectAfterChangeColumn() + public function testForeignKeyReferenceCorrectAfterChangeColumn(): void { $refTableColumnId = 'ref_table_id'; $refTableColumnToChange = 'columnToChange'; @@ -3046,7 +3046,7 @@ public function testForeignKeyReferenceCorrectAfterChangeColumn() $refTable->changeColumn($refTableColumnToChange, 'text')->save(); $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId])); - $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}")); + $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName())); $this->assertEquals('text', $this->adapter->getColumns($refTable->getName())[1]->getType()); $rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\''); @@ -3055,10 +3055,10 @@ public function testForeignKeyReferenceCorrectAfterChangeColumn() $sql = $row['sql']; } } - $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql); + $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql); } - public function testForeignKeyReferenceCorrectAfterRemoveColumn() + public function testForeignKeyReferenceCorrectAfterRemoveColumn(): void { $refTableColumnId = 'ref_table_id'; $refTableColumnToRemove = 'columnToRemove'; @@ -3073,7 +3073,7 @@ public function testForeignKeyReferenceCorrectAfterRemoveColumn() $refTable->removeColumn($refTableColumnToRemove)->save(); $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId])); - $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}")); + $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName())); $this->assertFalse($this->adapter->hasColumn($refTable->getName(), $refTableColumnToRemove)); $rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\''); @@ -3082,10 +3082,10 @@ public function testForeignKeyReferenceCorrectAfterRemoveColumn() $sql = $row['sql']; } } - $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql); + $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql); } - public function testForeignKeyReferenceCorrectAfterChangePrimaryKey() + public function testForeignKeyReferenceCorrectAfterChangePrimaryKey(): void { $refTableColumnAdditionalId = 'additional_id'; $refTableColumnId = 'ref_table_id'; @@ -3103,7 +3103,7 @@ public function testForeignKeyReferenceCorrectAfterChangePrimaryKey() ->save(); $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId])); - $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}")); + $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName())); $this->assertTrue($this->adapter->getColumns($refTable->getName())[1]->getIdentity()); $rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\''); @@ -3112,10 +3112,10 @@ public function testForeignKeyReferenceCorrectAfterChangePrimaryKey() $sql = $row['sql']; } } - $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql); + $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql); } - public function testForeignKeyReferenceCorrectAfterDropForeignKey() + public function testForeignKeyReferenceCorrectAfterDropForeignKey(): void { $refTableAdditionalColumnId = 'ref_table_additional_id'; $refTableAdditional = new Table('ref_table_additional', [], $this->adapter); @@ -3135,7 +3135,7 @@ public function testForeignKeyReferenceCorrectAfterDropForeignKey() $refTable->dropForeignKey($refTableAdditionalColumnId)->save(); $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId])); - $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}")); + $this->assertFalse($this->adapter->hasTable('tmp_' . $refTable->getName())); $this->assertFalse($this->adapter->hasForeignKey($refTable->getName(), [$refTableAdditionalColumnId])); $rows = $this->adapter->fetchAll('select * from sqlite_master where "type" = \'table\''); @@ -3144,17 +3144,17 @@ public function testForeignKeyReferenceCorrectAfterDropForeignKey() $sql = $row['sql']; } } - $this->assertStringContainsString("REFERENCES \"{$refTable->getName()}\" (\"id\")", $sql); + $this->assertStringContainsString(sprintf('REFERENCES "%s" ("id")', $refTable->getName()), $sql); } - public function testPdoExceptionUpdateNonExistingTable() + public function testPdoExceptionUpdateNonExistingTable(): void { $this->expectException(PDOException::class); $table = new Table('non_existing_table', [], $this->adapter); $table->addColumn('column', 'string')->update(); } - public function testAddCheckConstraint() + public function testAddCheckConstraint(): void { $table = new Table('check_table', [], $this->adapter); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) @@ -3166,7 +3166,7 @@ public function testAddCheckConstraint() $this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive')); } - public function testHasCheckConstraint() + public function testHasCheckConstraint(): void { $table = new Table('check_table3', [], $this->adapter); $table->addColumn('quantity', 'integer') @@ -3180,7 +3180,7 @@ public function testHasCheckConstraint() $this->assertTrue($this->adapter->hasCheckConstraint('check_table3', 'quantity_positive')); } - public function testDropCheckConstraint() + public function testDropCheckConstraint(): void { $table = new Table('check_table4', [], $this->adapter); $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) @@ -3194,7 +3194,7 @@ public function testDropCheckConstraint() $this->assertFalse($this->adapter->hasCheckConstraint('check_table4', 'price_check')); } - public function testCheckConstraintWithComplexExpression() + public function testCheckConstraintWithComplexExpression(): void { $table = new Table('check_table5', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -3211,10 +3211,10 @@ public function testCheckConstraintWithComplexExpression() // Verify the constraint is actually enforced $quotedTableName = $this->adapter->getConnection()->getDriver()->quoteIdentifier('check_table5'); $this->expectException(PDOException::class); - $this->adapter->execute("INSERT INTO {$quotedTableName} (email, status) VALUES ('test@example.com', 'invalid')"); + $this->adapter->execute(sprintf("INSERT INTO %s (email, status) VALUES ('test@example.com', 'invalid')", $quotedTableName)); } - public function testInsertOrSkipWithDuplicates() + public function testInsertOrSkipWithDuplicates(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -3237,7 +3237,7 @@ public function testInsertOrSkipWithDuplicates() $this->assertEquals('John', $rows[0]['name']); } - public function testInsertModeResetsAfterInsertOrSkip() + public function testInsertModeResetsAfterInsertOrSkip(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) @@ -3257,7 +3257,7 @@ public function testInsertModeResetsAfterInsertOrSkip() ])->save(); } - public function testBulkinsertOrSkipWithDuplicates() + public function testBulkinsertOrSkipWithDuplicates(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('sku', 'string', ['limit' => 50]) @@ -3284,7 +3284,7 @@ public function testBulkinsertOrSkipWithDuplicates() $this->assertEquals('30.00', $rows[2]['price']); } - public function testInsertOrSkipWithoutDuplicates() + public function testInsertOrSkipWithoutDuplicates(): void { $table = new Table('categories', [], $this->adapter); $table->addColumn('name', 'string') @@ -3300,7 +3300,7 @@ public function testInsertOrSkipWithoutDuplicates() $this->assertCount(2, $rows); } - public function testInsertOrUpdateWithDuplicates() + public function testInsertOrUpdateWithDuplicates(): void { $table = new Table('currencies', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 3]) @@ -3333,7 +3333,7 @@ public function testInsertOrUpdateWithDuplicates() $this->assertEquals('1.0500', $rows[2]['rate']); // USD updated } - public function testInsertOrUpdateWithMultipleUpdateColumns() + public function testInsertOrUpdateWithMultipleUpdateColumns(): void { $table = new Table('products', [], $this->adapter); $table->addColumn('sku', 'string', ['limit' => 50]) @@ -3358,7 +3358,7 @@ public function testInsertOrUpdateWithMultipleUpdateColumns() $this->assertEquals(50, $rows[0]['stock']); } - public function testInsertOrUpdateModeResetsAfterSave() + public function testInsertOrUpdateModeResetsAfterSave(): void { $table = new Table('items', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 10]) @@ -3378,7 +3378,7 @@ public function testInsertOrUpdateModeResetsAfterSave() ])->save(); } - public function testInsertOrUpdateRequiresConflictColumns() + public function testInsertOrUpdateRequiresConflictColumns(): void { $table = new Table('currencies', [], $this->adapter); $table->addColumn('code', 'string', ['limit' => 3]) diff --git a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php index f494d45d2..91dc7d0ae 100644 --- a/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqlserverAdapterTest.php @@ -22,13 +22,12 @@ class SqlserverAdapterTest extends TestCase { - /** - * @var \Migrations\Db\Adapter\SqlServerAdapter - */ - private $adapter; + private SqlserverAdapter $adapter; private array $config; + private StubConsoleOutput $out; + private ConsoleIo $io; protected function setUp(): void @@ -73,12 +72,12 @@ protected function getConsoleIo(): ConsoleIo return $this->io; } - public function testConnection() + public function testConnection(): void { $this->assertInstanceOf(Connection::class, $this->adapter->getConnection()); } - public function testCreatingTheSchemaTableOnConnect() + public function testCreatingTheSchemaTableOnConnect(): void { $this->adapter->connect(); $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); @@ -89,25 +88,25 @@ public function testCreatingTheSchemaTableOnConnect() $this->assertTrue($this->adapter->hasTable($this->adapter->getSchemaTableName())); } - public function testSchemaTableIsCreatedWithPrimaryKey() + public function testSchemaTableIsCreatedWithPrimaryKey(): void { $this->adapter->connect(); new Table($this->adapter->getSchemaTableName(), [], $this->adapter); $this->assertTrue($this->adapter->hasIndex($this->adapter->getSchemaTableName(), ['version'])); } - public function testQuoteTableName() + public function testQuoteTableName(): void { $this->assertEquals('[dbo].[test_table]', $this->adapter->quoteTableName('test_table')); $this->assertEquals('[schema].[table]', $this->adapter->quoteTableName('schema.table')); } - public function testQuoteColumnName() + public function testQuoteColumnName(): void { $this->assertEquals('[test_column]', $this->adapter->quoteColumnName('test_column')); } - public function testCreateTable() + public function testCreateTable(): void { $table = new Table('ntable', [], $this->adapter); $table->addColumn('realname', 'string') @@ -120,7 +119,7 @@ public function testCreateTable() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableWithSchema() + public function testCreateTableWithSchema(): void { $this->adapter->createSchema('nschema'); @@ -138,7 +137,7 @@ public function testCreateTableWithSchema() $this->adapter->dropSchema('nschema'); } - public function testCreateTableCustomIdColumn() + public function testCreateTableCustomIdColumn(): void { $table = new Table('ntable', ['id' => 'custom_id'], $this->adapter); $table->addColumn('realname', 'string') @@ -151,7 +150,7 @@ public function testCreateTableCustomIdColumn() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } - public function testCreateTableIdentityColumn() + public function testCreateTableIdentityColumn(): void { $table = new Table('ntable', ['id' => false, 'primary_key' => 'id'], $this->adapter); $table->addColumn('id', 'integer', ['identity' => true]) @@ -160,7 +159,7 @@ public function testCreateTableIdentityColumn() $this->assertTrue($this->adapter->hasColumn('ntable', 'id')); } - public function testCreateTableWithNoPrimaryKey() + public function testCreateTableWithNoPrimaryKey(): void { $options = [ 'id' => false, @@ -171,7 +170,7 @@ public function testCreateTableWithNoPrimaryKey() $this->assertFalse($this->adapter->hasColumn('atable', 'id')); } - public function testCreateTableWithConflictingPrimaryKeys() + public function testCreateTableWithConflictingPrimaryKeys(): void { $options = [ 'primary_key' => 'user_id', @@ -183,7 +182,7 @@ public function testCreateTableWithConflictingPrimaryKeys() $table->addColumn('user_id', 'integer')->save(); } - public function testCreateTableWithPrimaryKeySetToImplicitId() + public function testCreateTableWithPrimaryKeySetToImplicitId(): void { $options = [ 'primary_key' => 'id', @@ -195,7 +194,7 @@ public function testCreateTableWithPrimaryKeySetToImplicitId() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithPrimaryKeyArraySetToImplicitId() + public function testCreateTableWithPrimaryKeyArraySetToImplicitId(): void { $options = [ 'primary_key' => ['id'], @@ -207,7 +206,7 @@ public function testCreateTableWithPrimaryKeyArraySetToImplicitId() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId() + public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId(): void { $options = [ 'primary_key' => ['id', 'user_id'], @@ -218,7 +217,7 @@ public function testCreateTableWithMultiplePrimaryKeyArraySetToImplicitId() $table->addColumn('user_id', 'integer')->save(); } - public function testCreateTableWithMultiplePrimaryKeys() + public function testCreateTableWithMultiplePrimaryKeys(): void { $options = [ 'id' => false, @@ -233,7 +232,7 @@ public function testCreateTableWithMultiplePrimaryKeys() $this->assertFalse($this->adapter->hasIndex('table1', ['tag_id', 'user_email'])); } - public function testCreateTableWithPrimaryKeyAsUuid() + public function testCreateTableWithPrimaryKeyAsUuid(): void { $options = [ 'id' => false, @@ -247,7 +246,7 @@ public function testCreateTableWithPrimaryKeyAsUuid() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithPrimaryKeyAsBinaryUuid() + public function testCreateTableWithPrimaryKeyAsBinaryUuid(): void { $options = [ 'id' => false, @@ -261,7 +260,7 @@ public function testCreateTableWithPrimaryKeyAsBinaryUuid() $this->assertTrue($this->adapter->hasColumn('ztable', 'user_id')); } - public function testCreateTableWithMultipleIndexes() + public function testCreateTableWithMultipleIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -275,7 +274,7 @@ public function testCreateTableWithMultipleIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_name'])); } - public function testCreateTableWithUniqueIndexes() + public function testCreateTableWithUniqueIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -285,7 +284,7 @@ public function testCreateTableWithUniqueIndexes() $this->assertFalse($this->adapter->hasIndex('table1', ['email', 'user_email'])); } - public function testCreateTableWithNamedIndexes() + public function testCreateTableWithNamedIndexes(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -319,7 +318,7 @@ public function testCreateTableIndexWithWhere(): void $this->assertStringContainsString('([email]) WHERE is_verified = true', $indexQuery); } - public function testAddPrimaryKey() + public function testAddPrimaryKey(): void { $table = new Table('table1', ['id' => false], $this->adapter); $table @@ -333,7 +332,7 @@ public function testAddPrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testChangePrimaryKey() + public function testChangePrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -350,7 +349,7 @@ public function testChangePrimaryKey() $this->assertTrue($this->adapter->hasPrimaryKey('table1', ['column2', 'column3'])); } - public function testDropPrimaryKey() + public function testDropPrimaryKey(): void { $table = new Table('table1', ['id' => false, 'primary_key' => 'column1'], $this->adapter); $table @@ -364,7 +363,7 @@ public function testDropPrimaryKey() $this->assertFalse($this->adapter->hasPrimaryKey('table1', ['column1'])); } - public function testHasPrimaryKeyMultipleColumns() + public function testHasPrimaryKeyMultipleColumns(): void { $table = new Table('table1', ['id' => false, 'primary_key' => ['column1', 'column2', 'column3']], $this->adapter); $table @@ -378,7 +377,7 @@ public function testHasPrimaryKeyMultipleColumns() $this->assertFalse($table->hasPrimaryKey(['column1', 'column2', 'column3', 'column4'])); } - public function testHasPrimaryKeyCaseSensitivity() + public function testHasPrimaryKeyCaseSensitivity(): void { $table = new Table('table', ['id' => false, 'primary_key' => ['column1']], $this->adapter); $table @@ -389,7 +388,7 @@ public function testHasPrimaryKeyCaseSensitivity() $this->assertFalse($table->hasPrimaryKey('cOlUmN1')); } - public function testChangeCommentFails() + public function testChangeCommentFails(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -401,7 +400,7 @@ public function testChangeCommentFails() ->save(); } - public function testRenameTable() + public function testRenameTable(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -412,7 +411,7 @@ public function testRenameTable() $this->assertTrue($this->adapter->hasTable('table2')); } - public function testAddColumn() + public function testAddColumn(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -422,7 +421,7 @@ public function testAddColumn() $this->assertTrue($table->hasColumn('email')); } - public function testAddColumnWithDefaultValue() + public function testAddColumnWithDefaultValue(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -436,7 +435,7 @@ public function testAddColumnWithDefaultValue() } } - public function testAddColumnWithDefaultZero() + public function testAddColumnWithDefaultZero(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -451,7 +450,7 @@ public function testAddColumnWithDefaultZero() } } - public function testAddColumnWithDefaultNull() + public function testAddColumnWithDefaultNull(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -465,7 +464,7 @@ public function testAddColumnWithDefaultNull() } } - public function testAddColumnWithNotNullableNoDefault() + public function testAddColumnWithNotNullableNoDefault(): void { $table = new Table('table1', [], $this->adapter); $table @@ -480,7 +479,7 @@ public function testAddColumnWithNotNullableNoDefault() $this->assertNull($columns[1]->getDefault()); } - public function testAddColumnWithDefaultBool() + public function testAddColumnWithDefaultBool(): void { $table = new Table('table1', [], $this->adapter); $table->save(); @@ -499,7 +498,7 @@ public function testAddColumnWithDefaultBool() } } - public function testRenameColumn() + public function testRenameColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -511,7 +510,7 @@ public function testRenameColumn() $this->assertTrue($this->adapter->hasColumn('t', 'column2')); } - public function testRenamingANonExistentColumn() + public function testRenamingANonExistentColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -524,13 +523,13 @@ public function testRenamingANonExistentColumn() $this->assertInstanceOf( 'InvalidArgumentException', $e, - 'Expected exception of type InvalidArgumentException, got ' . get_class($e), + 'Expected exception of type InvalidArgumentException, got ' . $e::class, ); $this->assertEquals('The specified column does not exist: column2', $e->getMessage()); } } - public function testChangeColumnType() + public function testChangeColumnType(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -549,7 +548,7 @@ public function testChangeColumnType() } } - public function testChangeColumnNameAndNull() + public function testChangeColumnNameAndNull(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['null' => false]) @@ -569,7 +568,7 @@ public function testChangeColumnNameAndNull() } } - public function testChangeColumnDefaults() + public function testChangeColumnDefaults(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['default' => 'test']) @@ -591,7 +590,7 @@ public function testChangeColumnDefaults() $this->assertSame('another test', $columns[1]->getDefault()); } - public function testChangeColumnDefaultToNull() + public function testChangeColumnDefaultToNull(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string', ['null' => true, 'default' => 'test']) @@ -606,7 +605,7 @@ public function testChangeColumnDefaultToNull() $this->assertNull($columns[1]->getDefault()); } - public function testChangeColumnDefaultToZero() + public function testChangeColumnDefaultToZero(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'integer') @@ -621,7 +620,7 @@ public function testChangeColumnDefaultToZero() $this->assertSame(0, $columns[1]->getDefault()); } - public function testDropColumn() + public function testDropColumn(): void { $table = new Table('t', [], $this->adapter); $table->addColumn('column1', 'string') @@ -631,7 +630,7 @@ public function testDropColumn() $this->assertFalse($this->adapter->hasColumn('t', 'column1')); } - public static function columnsProvider() + public static function columnsProvider(): array { return [ ['column1', 'string', ['null' => true, 'default' => null]], @@ -655,7 +654,7 @@ public static function columnsProvider() } #[DataProvider('columnsProvider')] - public function testGetColumns($colName, $type, $options, $actualType = null) + public function testGetColumns(string $colName, string $type, array $options, ?string $actualType = null): void { $table = new Table('t', [], $this->adapter); $table @@ -671,7 +670,7 @@ public function testGetColumns($colName, $type, $options, $actualType = null) $this->assertEquals($actualType ?? $type, $columns[1]->getType()); } - public function testAddIndex() + public function testAddIndex(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -682,7 +681,7 @@ public function testAddIndex() $this->assertTrue($table->hasIndex('email')); } - public function testAddIndexWithSort() + public function testAddIndexWithSort(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -710,7 +709,7 @@ public function testAddIndexWithSort() $this->assertEquals($emailOrder['sort_order'], 'ASC'); } - public function testAddIndexWithIncludeColumns() + public function testAddIndexWithIncludeColumns(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('email', 'string') @@ -747,7 +746,7 @@ public function testAddIndexWithIncludeColumns() $this->assertEquals($emailOrder['included'], 1); } - public function testGetIndexes() + public function testGetIndexes(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -771,7 +770,7 @@ public function testGetIndexes() $this->assertEquals(['email', 'username'], $indexes[2]['columns']); } - public function testDropIndex() + public function testDropIndex(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -812,7 +811,7 @@ public function testDropIndex() $this->assertFalse($table4->hasIndex(['fname', 'lname'])); } - public function testDropIndexByName() + public function testDropIndexByName(): void { // single column index $table = new Table('table1', [], $this->adapter); @@ -838,7 +837,7 @@ public function testDropIndexByName() $this->assertFalse($table2->hasIndex(['fname', 'lname'])); } - public function testAddForeignKey() + public function testAddForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -856,7 +855,7 @@ public function testAddForeignKey() $this->assertTrue($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'], 'fk1')); } - public function testDropForeignKey() + public function testDropForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->addColumn('field1', 'string')->save(); @@ -872,7 +871,7 @@ public function testDropForeignKey() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_id'])); } - public function testDropForeignKeyWithMultipleColumns() + public function testDropForeignKeyWithMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -922,7 +921,7 @@ public function testDropForeignKeyWithMultipleColumns() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), ['ref_table_field1', 'ref_table_id'])); } - public function testDropForeignKeyWithIdenticalMultipleColumns() + public function testDropForeignKeyWithIdenticalMultipleColumns(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -969,11 +968,8 @@ public static function nonExistentForeignKeyColumnsProvider(): array ]; } - /** - * @param array $columns - */ #[DataProvider('nonExistentForeignKeyColumnsProvider')] - public function testDropForeignKeyByNonExistentKeyColumns(array $columns) + public function testDropForeignKeyByNonExistentKeyColumns(array $columns): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable @@ -1001,7 +997,7 @@ public function testDropForeignKeyByNonExistentKeyColumns(array $columns) $this->adapter->dropForeignKey($table->getName(), $columns); } - public function testDropForeignKeyCaseSensitivity() + public function testDropForeignKeyCaseSensitivity(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1023,7 +1019,7 @@ public function testDropForeignKeyCaseSensitivity() $this->adapter->dropForeignKey($table->getName(), ['ref_table_id']); } - public function testDropForeignKeyByName() + public function testDropForeignKeyByName(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1044,7 +1040,7 @@ public function testDropForeignKeyByName() } #[DataProvider('provideForeignKeysToCheck')] - public function testHasForeignKey($tableDef, $key, $exp) + public function testHasForeignKey(string $tableDef, string|array $key, bool $exp): void { $conn = $this->adapter->getConnection(); $conn->execute('CREATE TABLE other(a int, b int, c int, unique(a), unique(b), unique(a,b), unique(a,b,c));'); @@ -1052,7 +1048,7 @@ public function testHasForeignKey($tableDef, $key, $exp) $this->assertSame($exp, $this->adapter->hasForeignKey('t', $key)); } - public static function provideForeignKeysToCheck() + public static function provideForeignKeysToCheck(): array { return [ ['create table t(a int)', 'a', false], @@ -1079,7 +1075,7 @@ public static function provideForeignKeysToCheck() ]; } - public function testHasNamedForeignKey() + public function testHasNamedForeignKey(): void { $refTable = new Table('ref_table', [], $this->adapter); $refTable->save(); @@ -1102,13 +1098,13 @@ public function testHasNamedForeignKey() $this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'my_constraint2')); } - public function testHasDatabase() + public function testHasDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('fake_database_name')); $this->assertTrue($this->adapter->hasDatabase($this->config['database'])); } - public function testDropDatabase() + public function testDropDatabase(): void { $this->assertFalse($this->adapter->hasDatabase('phinx_temp_database')); $this->adapter->createDatabase('phinx_temp_database'); @@ -1116,13 +1112,13 @@ public function testDropDatabase() $this->adapter->dropDatabase('phinx_temp_database'); } - public function testCreateSchema() + public function testCreateSchema(): void { $this->adapter->createSchema('foo'); $this->assertTrue($this->adapter->hasSchema('foo')); } - public function testDropSchema() + public function testDropSchema(): void { $this->adapter->createSchema('foo'); $this->assertTrue($this->adapter->hasSchema('foo')); @@ -1130,7 +1126,7 @@ public function testDropSchema() $this->assertFalse($this->adapter->hasSchema('foo')); } - public function testDropAllSchemas() + public function testDropAllSchemas(): void { $this->adapter->createSchema('foo'); $this->adapter->createSchema('bar'); @@ -1142,14 +1138,14 @@ public function testDropAllSchemas() $this->assertFalse($this->adapter->hasSchema('bar')); } - public function testQuoteSchemaName() + public function testQuoteSchemaName(): void { $this->assertEquals('[schema]', $this->adapter->quoteSchemaName('schema')); // Dotted schema names are not supported $this->assertEquals('[schema].[schema]', $this->adapter->quoteSchemaName('schema.schema')); } - public function testAddColumnComment() + public function testAddColumnComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('field1', 'string', ['comment' => $comment = 'Comments from column "field1"']) @@ -1161,7 +1157,7 @@ public function testAddColumnComment() } #[Depends('testAddColumnComment')] - public function testChangeColumnComment() + public function testChangeColumnComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"']) @@ -1176,7 +1172,7 @@ public function testChangeColumnComment() } #[Depends('testAddColumnComment')] - public function testRemoveColumnComment() + public function testRemoveColumnComment(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('field1', 'string', ['comment' => 'Comments from column "field1"']) @@ -1193,7 +1189,7 @@ public function testRemoveColumnComment() /** * Test that column names are properly escaped when creating Foreign Keys */ - public function testForignKeysArePropertlyEscaped() + public function testForignKeysArePropertlyEscaped(): void { $userId = 'user'; $sessionId = 'session'; @@ -1209,7 +1205,7 @@ public function testForignKeysArePropertlyEscaped() $this->assertTrue($foreign->hasForeignKey('user')); } - public function testBulkInsertData() + public function testBulkInsertData(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1245,7 +1241,7 @@ public function testBulkInsertData() $this->assertEquals(3, $rows[2]['column2']); } - public function testBulkInsertLiteral() + public function testBulkInsertLiteral(): void { $data = [ [ @@ -1271,12 +1267,12 @@ public function testBulkInsertLiteral() $this->assertEquals('value1', $rows[0]['column1']); $this->assertEquals('value2', $rows[1]['column1']); $this->assertEquals('value3', $rows[2]['column1']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column2']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column2']); $this->assertEquals('2024-01-01 00:00:00.0000000', $rows[1]['column2']); $this->assertEquals('2025-01-01 00:00:00.0000000', $rows[2]['column2']); } - public function testInsertData() + public function testInsertData(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1309,7 +1305,7 @@ public function testInsertData() $this->assertEquals(3, $rows[2]['column2']); } - public function testInsertLiteral() + public function testInsertLiteral(): void { $data = [ [ @@ -1340,12 +1336,12 @@ public function testInsertLiteral() $this->assertEquals('test', $rows[0]['column2']); $this->assertEquals('test', $rows[1]['column2']); $this->assertEquals('foo', $rows[2]['column2']); - $this->assertMatchesRegularExpression('/[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/', $rows[0]['column3']); + $this->assertMatchesRegularExpression('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $rows[0]['column3']); $this->assertEquals('2024-01-01 00:00:00.0000000', $rows[1]['column3']); $this->assertEquals('2025-01-01 00:00:00.0000000', $rows[2]['column3']); } - public function testTruncateTable() + public function testTruncateTable(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('column1', 'string') @@ -1369,7 +1365,7 @@ public function testTruncateTable() $this->assertCount(0, $rows); } - public function testDumpCreateTableAndThenInsert() + public function testDumpCreateTableAndThenInsert(): void { $options = $this->adapter->getOptions(); $options['dryrun'] = true; @@ -1393,7 +1389,7 @@ public function testDumpCreateTableAndThenInsert() CREATE TABLE [dbo].[table1] ([column1] NVARCHAR(255) NOT NULL, [column2] INTEGER DEFAULT NULL, CONSTRAINT PK_table1 PRIMARY KEY ([column1])); INSERT INTO [dbo].[table1] ([column1], [column2]) VALUES ('id1', 1); OUTPUT; - $output = join("\n", $this->out->messages()); + $output = implode("\n", $this->out->messages()); $actualOutput = str_replace("\r\n", "\n", $output); $this->assertStringContainsString($expectedOutput, $actualOutput, 'Passing the --dry-run option does not dump create and then insert table queries to the output'); } @@ -1401,7 +1397,7 @@ public function testDumpCreateTableAndThenInsert() /** * Tests interaction with the query builder */ - public function testQueryBuilder() + public function testQueryBuilder(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -1442,7 +1438,7 @@ public function testQueryBuilder() $stm->closeCursor(); } - public function testQueryWithParams() + public function testQueryWithParams(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('string_col', 'string') @@ -1473,7 +1469,7 @@ public function testQueryWithParams() $this->assertEquals(3, $res[0]['c']); } - public function testIdentityInsert() + public function testIdentityInsert(): void { $table = new Table('table1', [], $this->adapter); $table->addColumn('name', 'string') @@ -1500,7 +1496,7 @@ public function testIdentityInsert() $this->assertEquals(50, $res[1]['id']); } - public function testInsertOrSkipThrowsException() + public function testInsertOrSkipThrowsException(): void { $table = new Table('users', [], $this->adapter); $table->addColumn('email', 'string', ['limit' => 255]) diff --git a/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php b/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php index d7b30fc83..223609ad5 100644 --- a/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php +++ b/tests/TestCase/Db/Adapter/UnifiedMigrationsTableStorageTest.php @@ -21,12 +21,7 @@ */ class UnifiedMigrationsTableStorageTest extends TestCase { - /** - * @var bool - */ - private bool $tableCreated = false; - - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -37,7 +32,7 @@ public function setUp(): void $this->cleanupTable(); } - public function tearDown(): void + protected function tearDown(): void { // Always clean up the table $this->cleanupTable(); @@ -74,7 +69,7 @@ private function cleanupTable(): void // Also drop any phinxlog tables that might exist $connection->execute('DROP TABLE IF EXISTS phinxlog'); $connection->execute('DROP TABLE IF EXISTS migrator_phinxlog'); - } catch (Exception $e) { + } catch (Exception) { // Ignore cleanup errors } } @@ -96,7 +91,6 @@ public function testMigrateCreatesUnifiedTable(): void $dialect = $connection->getDriver()->schemaDialect(); $this->assertTrue($dialect->hasTable(UnifiedMigrationsTableStorage::TABLE_NAME)); - $this->tableCreated = true; // Verify records were inserted with null plugin (app migrations) $result = $connection->selectQuery() @@ -120,7 +114,6 @@ public function testMigratePluginUsesUnifiedTable(): void // Run app migrations first to create the table $this->exec('migrations migrate -c test --source Migrations --no-lock'); $this->assertExitSuccess(); - $this->tableCreated = true; // Clear the migration records for app (but keep the table) $this->clearMigrationRecords('test'); @@ -148,7 +141,6 @@ public function testRollbackWithUnifiedTable(): void // Run migrations $this->exec('migrations migrate -c test --source Migrations --no-lock'); $this->assertExitSuccess(); - $this->tableCreated = true; // Verify we have records $initialCount = $this->getMigrationRecordCount('test'); @@ -168,7 +160,6 @@ public function testStatusWithUnifiedTable(): void // Run migrations $this->exec('migrations migrate -c test --source Migrations --no-lock'); $this->assertExitSuccess(); - $this->tableCreated = true; // Check status $this->exec('migrations status -c test --source Migrations'); @@ -183,7 +174,6 @@ public function testAppAndPluginMigrationsAreSeparated(): void // Run app migrations $this->exec('migrations migrate -c test --source Migrations --no-lock'); $this->assertExitSuccess(); - $this->tableCreated = true; // Run plugin migrations $this->exec('migrations migrate -c test --plugin Migrator --no-lock'); diff --git a/tests/TestCase/Db/LiteralTest.php b/tests/TestCase/Db/LiteralTest.php index e8c55fdb5..1ff0c617a 100644 --- a/tests/TestCase/Db/LiteralTest.php +++ b/tests/TestCase/Db/LiteralTest.php @@ -8,18 +8,18 @@ class LiteralTest extends TestCase { - public function testToString() + public function testToString(): void { $str = 'test1'; $instance = new Literal($str); $this->assertEquals($str, (string)$instance); } - public function testFrom() + public function testFrom(): void { $str = 'test1'; $instance = Literal::from($str); - $this->assertInstanceOf('\Migrations\Db\Literal', $instance); + $this->assertInstanceOf(Literal::class, $instance); $this->assertEquals($str, (string)$instance); } } diff --git a/tests/TestCase/Db/Table/ColumnTest.php b/tests/TestCase/Db/Table/ColumnTest.php index 2d5ccd1ae..045cf21ce 100644 --- a/tests/TestCase/Db/Table/ColumnTest.php +++ b/tests/TestCase/Db/Table/ColumnTest.php @@ -22,7 +22,7 @@ protected function tearDown(): void Configure::delete('Migrations.unsigned_ints'); } - public function testNullConstructorParameter() + public function testNullConstructorParameter(): void { $column = new Column(name: 'title'); $this->assertTrue($column->isNull()); @@ -38,7 +38,7 @@ public function testNullConstructorParameter() $this->assertTrue($column->isNull()); } - public function testSetOptionThrowsExceptionIfOptionIsNotString() + public function testSetOptionThrowsExceptionIfOptionIsNotString(): void { $column = new Column(); @@ -48,7 +48,7 @@ public function testSetOptionThrowsExceptionIfOptionIsNotString() $column->setOptions(['identity']); } - public function testSetOptionsIdentity() + public function testSetOptionsIdentity(): void { $column = new Column(); $this->assertTrue($column->isNull()); @@ -59,7 +59,7 @@ public function testSetOptionsIdentity() $this->assertTrue($column->isIdentity()); } - public function testColumnNullFeatureFlag() + public function testColumnNullFeatureFlag(): void { $column = new Column(); $this->assertTrue($column->isNull()); diff --git a/tests/TestCase/Db/Table/ForeignKeyTest.php b/tests/TestCase/Db/Table/ForeignKeyTest.php index c2fce98ab..4c9fc179e 100644 --- a/tests/TestCase/Db/Table/ForeignKeyTest.php +++ b/tests/TestCase/Db/Table/ForeignKeyTest.php @@ -11,10 +11,7 @@ class ForeignKeyTest extends TestCase { - /** - * @var ForeignKey - */ - private $fk; + private ForeignKey $fk; protected function setUp(): void { @@ -38,7 +35,7 @@ public function testReferencedColumns(): void $this->assertEquals(['user_id', 'tenant_id'], $this->fk->getReferencedColumns()); } - public function testOnDeleteSetNullCanBeSetThroughOptions() + public function testOnDeleteSetNullCanBeSetThroughOptions(): void { $this->assertEquals( ForeignKey::SET_NULL, @@ -46,30 +43,22 @@ public function testOnDeleteSetNullCanBeSetThroughOptions() ); } - public function testInitiallyActionsEmpty() + public function testInitiallyActionsEmpty(): void { $this->assertSame(ForeignKey::NO_ACTION, $this->fk->getOnDelete()); $this->assertSame(ForeignKey::NO_ACTION, $this->fk->getOnUpdate()); } - /** - * @param string $dirtyValue - * @param string $valueOfConstant - */ #[DataProvider('actionsProvider')] - public function testBothActionsCanBeSetThroughSetters($dirtyValue, $valueOfConstant) + public function testBothActionsCanBeSetThroughSetters(string $dirtyValue, string $valueOfConstant): void { $this->fk->setOnDelete($dirtyValue)->setOnUpdate($dirtyValue); $this->assertEquals($valueOfConstant, $this->fk->getOnDelete()); $this->assertEquals($valueOfConstant, $this->fk->getOnUpdate()); } - /** - * @param string $dirtyValue - * @param string $valueOfConstant - */ #[DataProvider('actionsProvider')] - public function testBothActionsCanBeSetThroughOptions($dirtyValue, $valueOfConstant) + public function testBothActionsCanBeSetThroughOptions(string $dirtyValue, string $valueOfConstant): void { $this->fk->setOptions([ 'delete' => $dirtyValue, @@ -79,21 +68,21 @@ public function testBothActionsCanBeSetThroughOptions($dirtyValue, $valueOfConst $this->assertEquals($valueOfConstant, $this->fk->getOnUpdate()); } - public function testUnknownActionsNotAllowedThroughSetter() + public function testUnknownActionsNotAllowedThroughSetter(): void { $this->expectException(InvalidArgumentException::class); $this->fk->setOnDelete('i m dump'); } - public function testUnknownActionsNotAllowedThroughOptions() + public function testUnknownActionsNotAllowedThroughOptions(): void { $this->expectException(InvalidArgumentException::class); $this->fk->setOptions(['update' => 'no yu a dumb']); } - public static function actionsProvider() + public static function actionsProvider(): array { return [ [ForeignKey::CASCADE, ForeignKey::CASCADE], @@ -107,7 +96,7 @@ public static function actionsProvider() ]; } - public function testSetOptionThrowsExceptionIfOptionIsNotString() + public function testSetOptionThrowsExceptionIfOptionIsNotString(): void { $this->expectException(RuntimeException::class); $this->expectExceptionMessage('"0" is not a valid foreign key option'); diff --git a/tests/TestCase/Db/Table/IndexTest.php b/tests/TestCase/Db/Table/IndexTest.php index 6c9011c20..b42c3e136 100644 --- a/tests/TestCase/Db/Table/IndexTest.php +++ b/tests/TestCase/Db/Table/IndexTest.php @@ -9,7 +9,7 @@ class IndexTest extends TestCase { - public function testSetOptionThrowsExceptionIfOptionIsNotString() + public function testSetOptionThrowsExceptionIfOptionIsNotString(): void { $column = new Index(); diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 463e9aa86..3bd3e5cba 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -49,7 +49,7 @@ public static function provideTimestampColumnNames(): array return $result; } - public function testAddColumnWithAnInvalidColumnType() + public function testAddColumnWithAnInvalidColumnType(): void { try { $adapter = new MysqlAdapter([]); @@ -61,13 +61,13 @@ public function testAddColumnWithAnInvalidColumnType() $this->assertInstanceOf( 'InvalidArgumentException', $e, - 'Expected exception of type InvalidArgumentException, got ' . get_class($e), + 'Expected exception of type InvalidArgumentException, got ' . $e::class, ); $this->assertStringStartsWith('An invalid column type ', $e->getMessage()); } } - public function testAddColumnWithColumnObject() + public function testAddColumnWithColumnObject(): void { $adapter = new MysqlAdapter([]); $column = new Column(); @@ -75,12 +75,13 @@ public function testAddColumnWithColumnObject() ->setType('integer'); $table = new Table('ntable', [], $adapter); $table->addColumn($column); + $actions = $this->getPendingActions($table); $this->assertInstanceOf(AddColumn::class, $actions[0]); $this->assertSame($column, $actions[0]->getColumn()); } - public function testAddColumnWithNoAdapterSpecified() + public function testAddColumnWithNoAdapterSpecified(): void { try { $table = new Table('ntable'); @@ -90,12 +91,12 @@ public function testAddColumnWithNoAdapterSpecified() $this->assertInstanceOf( 'RuntimeException', $e, - 'Expected exception of type RuntimeException, got ' . get_class($e), + 'Expected exception of type RuntimeException, got ' . $e::class, ); } } - public function testAddComment() + public function testAddComment(): void { $adapter = new MysqlAdapter([]); $table = new Table('ntable', ['comment' => 'test comment'], $adapter); @@ -103,7 +104,7 @@ public function testAddComment() $this->assertEquals('test comment', $options['comment']); } - public function testAddIndexWithIndexObject() + public function testAddIndexWithIndexObject(): void { $adapter = new MysqlAdapter([]); $index = new Index(); @@ -111,6 +112,7 @@ public function testAddIndexWithIndexObject() ->setColumns(['email']); $table = new Table('ntable', [], $adapter); $table->addIndex($index); + $actions = $this->getPendingActions($table); $this->assertInstanceOf(AddIndex::class, $actions[0]); $this->assertSame($index, $actions[0]->getIndex()); @@ -158,7 +160,6 @@ public function testAddForeignKeyWithObject(): void } /** - * @param AdapterInterface $adapter * @param string|null $createdAtColumnName * @param string|null $updatedAtColumnName * @param string $expectedCreatedAtColumnName @@ -168,14 +169,15 @@ public function testAddForeignKeyWithObject(): void #[DataProvider('provideTimestampColumnNames')] public function testAddTimestamps( AdapterInterface $adapter, - $createdAtColumnName, - $updatedAtColumnName, + string|bool|null $createdAtColumnName, + string|bool|null $updatedAtColumnName, $expectedCreatedAtColumnName, $expectedUpdatedAtColumnName, $withTimezone, ): void { $table = new Table('ntable', [], $adapter); $table->addTimestamps($createdAtColumnName, $updatedAtColumnName, $withTimezone); + $actions = $this->getPendingActions($table); $columns = []; @@ -198,14 +200,12 @@ public function testAddTimestamps( $this->assertNull($columns[1]->getDefault()); } - /** - * @param AdapterInterface $adapter - */ #[DataProvider('provideAdapters')] - public function testAddTimestampsNoUpdated(AdapterInterface $adapter) + public function testAddTimestampsNoUpdated(AdapterInterface $adapter): void { $table = new Table('ntable', [], $adapter); $table->addTimestamps(null, false); + $actions = $this->getPendingActions($table); $columns = []; @@ -223,14 +223,12 @@ public function testAddTimestampsNoUpdated(AdapterInterface $adapter) $this->assertSame('', $columns[0]->getUpdate()); } - /** - * @param AdapterInterface $adapter - */ #[DataProvider('provideAdapters')] - public function testAddTimestampsNoCreated(AdapterInterface $adapter) + public function testAddTimestampsNoCreated(AdapterInterface $adapter): void { $table = new Table('ntable', [], $adapter); $table->addTimestamps(false, null); + $actions = $this->getPendingActions($table); $columns = []; @@ -249,11 +247,8 @@ public function testAddTimestampsNoCreated(AdapterInterface $adapter) $this->assertNull($columns[0]->getDefault()); } - /** - * @param AdapterInterface $adapter - */ #[DataProvider('provideAdapters')] - public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter) + public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter): void { $table = new Table('ntable', [], $adapter); $this->expectException(RuntimeException::class); @@ -262,7 +257,6 @@ public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter) } /** - * @param AdapterInterface $adapter * @param string|null $createdAtColumnName * @param string|null $updatedAtColumnName * @param string $expectedCreatedAtColumnName @@ -272,14 +266,15 @@ public function testAddTimestampsThrowsOnBothFalse(AdapterInterface $adapter) #[DataProvider('provideTimestampColumnNames')] public function testAddTimestampsWithTimezone( AdapterInterface $adapter, - $createdAtColumnName, - $updatedAtColumnName, + string|bool|null $createdAtColumnName, + string|bool|null $updatedAtColumnName, $expectedCreatedAtColumnName, $expectedUpdatedAtColumnName, $withTimezone, ): void { $table = new Table('ntable', [], $adapter); $table->addTimestampsWithTimezone($createdAtColumnName, $updatedAtColumnName); + $actions = $this->getPendingActions($table); $columns = []; @@ -302,7 +297,7 @@ public function testAddTimestampsWithTimezone( $this->assertNull($columns[1]->getDefault()); } - public function testInsert() + public function testInsert(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -319,7 +314,7 @@ public function testInsert() $this->assertEquals($expectedData, $table->getData()); } - public function testInsertMultipleRowsWithoutZeroKey() + public function testInsertMultipleRowsWithoutZeroKey(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -340,7 +335,7 @@ public function testInsertMultipleRowsWithoutZeroKey() $this->assertEquals($expectedData, $table->getData()); } - public function testInsertSaveEmptyData() + public function testInsertSaveEmptyData(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -352,7 +347,7 @@ public function testInsertSaveEmptyData() $table->insert([])->save(); } - public function testInsertSaveData() + public function testInsertSaveData(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -385,7 +380,7 @@ public function testInsertSaveData() ->save(); } - public function testSaveAfterSaveData() + public function testSaveAfterSaveData(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -418,31 +413,29 @@ public function testSaveAfterSaveData() ->save(); } - public function testResetAfterAddingData() + public function testResetAfterAddingData(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) ->getMock(); $table = new Table('ntable', [], $adapterStub); $columns = ['column1']; - $data = [['value1']]; - $table->insert($columns, $data)->save(); + $table->insert($columns)->save(); $this->assertEquals([], $table->getData()); } - public function testPendingAfterAddingData() + public function testPendingAfterAddingData(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) ->getMock(); $table = new Table('ntable', [], $adapterStub); $columns = ['column1']; - $data = [['value1']]; - $table->insert($columns, $data); + $table->insert($columns); $this->assertTrue($table->hasPendingActions()); } - public function testPendingAfterAddingColumn() + public function testPendingAfterAddingColumn(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -455,7 +448,7 @@ public function testPendingAfterAddingColumn() $this->assertTrue($table->hasPendingActions()); } - public function testGetColumn() + public function testGetColumn(): void { $adapterStub = $this->getMockBuilder(MysqlAdapter::class) ->setConstructorArgs([[]]) @@ -477,23 +470,22 @@ public function testGetColumn() /** * @param string $indexIdentifier - * @param Index $index */ #[DataProvider('removeIndexDataprovider')] - public function testRemoveIndex($indexIdentifier, Index $index) + public function testRemoveIndex(string|array $indexIdentifier, Index $index): void { $adapter = new MysqlAdapter([]); $table = new Table('table', [], $adapter); $table->removeIndex($indexIdentifier); - $indexes = array_map(function (DropIndex $action) { + $indexes = array_map(function (DropIndex $action): Index { return $action->getIndex(); }, $this->getPendingActions($table)); $this->assertEquals([$index], $indexes); } - public static function removeIndexDataprovider() + public static function removeIndexDataprovider(): array { return [ [ @@ -513,8 +505,7 @@ public static function removeIndexDataprovider() protected function getPendingActions($table) { - $prop = new ReflectionProperty(get_class($table), 'actions'); - $prop->setAccessible(true); + $prop = new ReflectionProperty($table::class, 'actions'); return $prop->getValue($table)->getActions(); } diff --git a/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php b/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php index 9db05c0cc..e13e6f485 100644 --- a/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php +++ b/tests/TestCase/Middleware/PendingMigrationsMiddlewareTest.php @@ -37,7 +37,7 @@ class PendingMigrationsMiddlewareTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); $connection = ConnectionManager::get('test'); @@ -51,7 +51,7 @@ public function setUp(): void ); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); ConnectionManager::drop('custom'); @@ -66,7 +66,7 @@ public function testAppMigrationsFail(): void $middleware = new PendingMigrationsMiddleware(); $request = new ServerRequest(); - $handler = new TestRequestHandler(function ($req) { + $handler = new TestRequestHandler(function ($req): Response { return new Response(); }); @@ -95,11 +95,12 @@ public function testAppMigrationsSuccess(): void ], ]; $config = new Config($config); + $manager = new Manager($config, $this->io); $manager->migrate(null, true); $request = new ServerRequest(); - $handler = new TestRequestHandler(function ($req) { + $handler = new TestRequestHandler(function ($req): Response { return new Response(); }); $result = $middleware->process($request, $handler); @@ -118,7 +119,7 @@ public function testAppAndPluginsMigrationsFail(): void ]); $request = new ServerRequest(); - $handler = new TestRequestHandler(function ($req) { + $handler = new TestRequestHandler(function ($req): Response { return new Response(); }); @@ -151,11 +152,12 @@ public function testAppAndPluginsMigrationsSuccess(): void ], ]; $config = new Config($config); + $manager = new Manager($config, $this->io); $manager->migrate(null, true); $request = new ServerRequest(); - $handler = new TestRequestHandler(function ($req) { + $handler = new TestRequestHandler(function ($req): Response { return new Response(); }); diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index 84d060582..633c20f8b 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -26,26 +26,26 @@ protected function setUp(): void $this->environment = new Environment('test', []); } - public function testConstructorWorksAsExpected() + public function testConstructorWorksAsExpected(): void { $env = new Environment('testenv', ['foo' => 'bar']); $this->assertEquals('testenv', $env->getName()); $this->assertArrayHasKey('foo', $env->getOptions()); } - public function testSettingTheName() + public function testSettingTheName(): void { $this->environment->setName('prod123'); $this->assertEquals('prod123', $this->environment->getName()); } - public function testSettingOptions() + public function testSettingOptions(): void { $this->environment->setOptions(['foo' => 'bar']); $this->assertArrayHasKey('foo', $this->environment->getOptions()); } - public function testInvalidAdapter() + public function testInvalidAdapter(): void { $this->environment->setOptions(['adapter' => 'fakeadapter']); @@ -55,14 +55,14 @@ public function testInvalidAdapter() $this->environment->getAdapter(); } - public function testNoAdapter() + public function testNoAdapter(): void { $this->expectException(RuntimeException::class); $this->environment->getAdapter(); } - public function testGetAdapterWithBadConnectionName() + public function testGetAdapterWithBadConnectionName(): void { $this->environment->setOptions(['connection' => 'lolnope']); @@ -72,7 +72,7 @@ public function testGetAdapterWithBadConnectionName() $this->environment->getAdapter(); } - public function testGetAdapter() + public function testGetAdapter(): void { /** @var array $config */ $config = ConnectionManager::getConfig('test'); @@ -86,7 +86,7 @@ public function testGetAdapter() $this->assertInstanceOf(AdapterWrapper::class, $adapter); } - public function testSchemaName() + public function testSchemaName(): void { $this->assertEquals('phinxlog', $this->environment->getSchemaTableName()); @@ -94,7 +94,7 @@ public function testSchemaName() $this->assertEquals('changelog', $this->environment->getSchemaTableName()); } - public function testCurrentVersion() + public function testCurrentVersion(): void { $stub = $this->getMockBuilder(AbstractAdapter::class) ->setConstructorArgs([[]]) @@ -108,7 +108,7 @@ public function testCurrentVersion() $this->assertEquals(20110301080000, $this->environment->getCurrentVersion()); } - public function testExecutingAMigrationUp() + public function testExecutingAMigrationUp(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -123,6 +123,7 @@ public function testExecutingAMigrationUp() // up $upMigration = new class (20110301080000) extends BaseMigration { public bool $executed = false; + public function up(): void { $this->executed = true; @@ -133,7 +134,7 @@ public function up(): void $this->assertTrue($upMigration->executed); } - public function testExecutingAMigrationDown() + public function testExecutingAMigrationDown(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -148,6 +149,7 @@ public function testExecutingAMigrationDown() // down $downMigration = new class (20110301080000) extends BaseMigration { public bool $executed = false; + public function down(): void { $this->executed = true; @@ -158,7 +160,7 @@ public function down(): void $this->assertTrue($downMigration->executed); } - public function testExecutingAMigrationWithTransactions() + public function testExecutingAMigrationWithTransactions(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -179,6 +181,7 @@ public function testExecutingAMigrationWithTransactions() // migrate $migration = new class (20110301080000) extends BaseMigration { public bool $executed = false; + public function up(): void { $this->executed = true; @@ -189,7 +192,7 @@ public function up(): void $this->assertTrue($migration->executed); } - public function testExecutingAMigrationWithUseTransactions() + public function testExecutingAMigrationWithUseTransactions(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -226,7 +229,7 @@ public function up(): void $this->assertTrue($migration->executed); } - public function testExecutingAChangeMigrationUp() + public function testExecutingAChangeMigrationUp(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -241,6 +244,7 @@ public function testExecutingAChangeMigrationUp() // migration $migration = new class (20130301080000) extends BaseMigration { public bool $executed = false; + public function change(): void { $this->executed = true; @@ -251,7 +255,7 @@ public function change(): void $this->assertTrue($migration->executed); } - public function testExecutingAChangeMigrationDown() + public function testExecutingAChangeMigrationDown(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -266,6 +270,7 @@ public function testExecutingAChangeMigrationDown() // migration $migration = new class (20130301080000) extends BaseMigration { public bool $executed = false; + public function change(): void { $this->executed = true; @@ -276,7 +281,7 @@ public function change(): void $this->assertTrue($migration->executed); } - public function testExecutingAFakeMigration() + public function testExecutingAFakeMigration(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -291,6 +296,7 @@ public function testExecutingAFakeMigration() // migration $migration = new class (20130301080000) extends BaseMigration { public bool $executed = false; + public function change(): void { $this->executed = true; @@ -301,7 +307,7 @@ public function change(): void $this->assertFalse($migration->executed); } - public function testGettingInputObject() + public function testGettingInputObject(): void { $mock = $this->getMockBuilder(ConsoleIo::class)->getMock(); $this->environment->setIo($mock); @@ -309,7 +315,7 @@ public function testGettingInputObject() $this->assertInstanceOf(ConsoleIo::class, $inputObject); } - public function testExecuteMigrationCallsInit() + public function testExecuteMigrationCallsInit(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -324,6 +330,7 @@ public function testExecuteMigrationCallsInit() // up $upMigration = new class (20110301080000) extends BaseMigration { public bool $initExecuted = false; + public bool $upExecuted = false; public function init(): void @@ -341,7 +348,7 @@ public function up(): void $this->assertTrue($upMigration->upExecuted); } - public function testExecuteSeedInit() + public function testExecuteSeedInit(): void { // stub adapter $adapterStub = $this->getMockBuilder(AbstractAdapter::class) @@ -352,6 +359,7 @@ public function testExecuteSeedInit() $seed = new class (20110301080000) extends BaseSeed { public bool $initExecuted = false; + public bool $runExecuted = false; public function init(): void diff --git a/tests/TestCase/Migration/ManagerFactoryTest.php b/tests/TestCase/Migration/ManagerFactoryTest.php index 0352e887f..12bdd2dbf 100644 --- a/tests/TestCase/Migration/ManagerFactoryTest.php +++ b/tests/TestCase/Migration/ManagerFactoryTest.php @@ -6,6 +6,8 @@ use Cake\Console\ConsoleIo; use Cake\Console\TestSuite\StubConsoleInput; use Cake\Console\TestSuite\StubConsoleOutput; +use Cake\Database\Connection; +use Cake\Database\Driver\Mysql; use Cake\Datasource\ConnectionManager; use Migrations\Migration\ManagerFactory; use PHPUnit\Framework\TestCase; @@ -16,6 +18,7 @@ public function testConnection(): void { $out = new StubConsoleOutput(); $out->setOutputAs(StubConsoleOutput::PLAIN); + $in = new StubConsoleInput([]); $io = new ConsoleIo($out, $out, $in); @@ -41,6 +44,7 @@ public function testDsnConnection(): void { $out = new StubConsoleOutput(); $out->setOutputAs(StubConsoleOutput::PLAIN); + $in = new StubConsoleInput([]); $io = new ConsoleIo($out, $out, $in); @@ -53,9 +57,9 @@ public function testDsnConnection(): void 'scheme' => 'mysql', 'username' => 'root', 'host' => '127.0.0.1', - 'className' => 'Cake\Database\Connection', + 'className' => Connection::class, 'database' => 'db_tmp', - 'driver' => 'Cake\Database\Driver\Mysql', + 'driver' => Mysql::class, 'name' => 'tmp', ]; $this->assertEquals($expected, ConnectionManager::getConfig('tmp')); diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index 4efcf70c2..3d7ce3d44 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -13,6 +13,7 @@ use Migrations\Db\Adapter\AdapterInterface; use Migrations\Migration\Environment; use Migrations\Migration\Manager; +use Migrations\MigrationInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -40,10 +41,7 @@ class ManagerTest extends TestCase */ protected $in; - /** - * @var Manager - */ - private $manager; + private ?Manager $manager = null; protected function setUp(): void { @@ -51,6 +49,7 @@ protected function setUp(): void $this->out = new StubConsoleOutput(); $this->out->setOutputAs(StubConsoleOutput::PLAIN); + $this->in = new StubConsoleInput([]); $this->io = new ConsoleIo($this->out, $this->out, $this->in); @@ -70,9 +69,9 @@ protected static function getDriverType(): string protected function getOutput(): string { $lines = $this->out->messages(); - $lines = array_map(fn($line) => strip_tags($line), $lines); + $lines = array_map(strip_tags(...), $lines); - return join("\n", $lines); + return implode("\n", $lines); } protected function tearDown(): void @@ -80,11 +79,6 @@ protected function tearDown(): void $this->manager = null; } - private static function getCorrectedPath(string $path): string - { - return str_replace('/', DIRECTORY_SEPARATOR, $path); - } - /** * Returns a sample configuration array for use with the unit tests. * @@ -306,7 +300,7 @@ public function testPrintStatusMethodWithBreakpointSet(): void $this->assertEquals($expected, $return); } - public function testPrintStatusMethodWithNoMigrations() + public function testPrintStatusMethodWithNoMigrations(): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -325,7 +319,7 @@ public function testPrintStatusMethodWithNoMigrations() $this->assertEquals([], $return); } - public function testPrintStatusMethodWithMissingMigrations() + public function testPrintStatusMethodWithMissingMigrations(): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -384,7 +378,7 @@ public function testPrintStatusMethodWithMissingMigrations() $this->assertEquals($expected, $return); } - public function testPrintStatusMethodWithMissingLastMigration() + public function testPrintStatusMethodWithMissingLastMigration(): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -445,7 +439,7 @@ public function testPrintStatusMethodWithMissingLastMigration() $this->assertEquals($expected, $return); } - public function testPrintStatusMethodWithMissingMigrationsAndBreakpointSet() + public function testPrintStatusMethodWithMissingMigrationsAndBreakpointSet(): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -504,7 +498,7 @@ public function testPrintStatusMethodWithMissingMigrationsAndBreakpointSet() $this->assertEquals($expected, $return); } - public function testPrintStatusMethodWithDownMigrations() + public function testPrintStatusMethodWithDownMigrations(): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -540,7 +534,7 @@ public function testPrintStatusMethodWithDownMigrations() $this->assertEquals($expected, $return); } - public function testPrintStatusMethodWithMissingAndDownMigrations() + public function testPrintStatusMethodWithMissingAndDownMigrations(): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -605,7 +599,7 @@ public function testPrintStatusMethodWithMissingAndDownMigrations() $this->assertEquals($expected, $return); } - public function testGetMigrationsWithDuplicateMigrationVersions() + public function testGetMigrationsWithDuplicateMigrationVersions(): void { $config = new Config(['paths' => ['migrations' => ROOT . '/config/Duplicateversions']]); $manager = new Manager($config, $this->io); @@ -616,7 +610,7 @@ public function testGetMigrationsWithDuplicateMigrationVersions() $manager->getMigrations(); } - public function testGetMigrationsWithDuplicateMigrationNames() + public function testGetMigrationsWithDuplicateMigrationNames(): void { $config = new Config(['paths' => ['migrations' => ROOT . '/config/Duplicatenames']]); $manager = new Manager($config, $this->io); @@ -627,7 +621,7 @@ public function testGetMigrationsWithDuplicateMigrationNames() $manager->getMigrations(); } - public function testGetMigrationsWithInvalidMigrationClassName() + public function testGetMigrationsWithInvalidMigrationClassName(): void { $config = new Config(['paths' => ['migrations' => ROOT . '/config/Invalidclassname']]); $manager = new Manager($config, $this->io); @@ -651,7 +645,7 @@ public function testGetMigrationsWithLegacyAbstractMigrationClass(): void $manager->getMigrations(); } - public function testGetMigrationsWithAnonymousClass() + public function testGetMigrationsWithAnonymousClass(): void { $config = new Config(['paths' => ['migrations' => ROOT . '/config/AnonymousMigrations']]); $manager = new Manager($config, $this->io); @@ -665,13 +659,13 @@ public function testGetMigrationsWithAnonymousClass() $migration = reset($migrations); // Check that it's a valid migration object - $this->assertInstanceOf('\Migrations\MigrationInterface', $migration); + $this->assertInstanceOf(MigrationInterface::class, $migration); // Check the version was set correctly (2024_12_08_150000 => 20241208150000) $this->assertEquals(20241208150000, $migration->getVersion()); } - public function testGettingAValidEnvironment() + public function testGettingAValidEnvironment(): void { $this->assertInstanceOf( Environment::class, @@ -684,12 +678,9 @@ public function testGettingAValidEnvironment() * migration to point to. * * @param string[] $availableMigrations - * @param string $dateString - * @param string $expectedMigration - * @param string $message */ #[DataProvider('migrateDateDataProvider')] - public function testMigrationsByDate(array $availableMigrations, $dateString, $expectedMigration, $message) + public function testMigrationsByDate(array $availableMigrations, string $dateString, string $expectedMigration, string $message): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -719,12 +710,10 @@ public function testMigrationsByDate(array $availableMigrations, $dateString, $e * migration to point to. * * @param string[] $availableMigrations - * @param int $count * @param string $expectedMigration - * @param string $message */ #[DataProvider('migrateByCountDataProvider')] - public function testMigrationsByCount(array $availableMigrations, $count, $expectedMigration, $message) + public function testMigrationsByCount(array $availableMigrations, int $count, ?string $expectedMigration, string $message): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -744,7 +733,7 @@ public function testMigrationsByCount(array $availableMigrations, $count, $expec // Mock getMigrations to return the available migrations $migrations = []; foreach ($availableMigrations as $version) { - $migration = $this->getMockBuilder('\Migrations\MigrationInterface') + $migration = $this->getMockBuilder(MigrationInterface::class) ->getMock(); $migration->expects($this->any()) ->method('getVersion') @@ -758,11 +747,11 @@ public function testMigrationsByCount(array $availableMigrations, $count, $expec // Use reflection to set the migrations property $reflection = new ReflectionClass($this->manager); $migrationsProperty = $reflection->getProperty('migrations'); - $migrationsProperty->setAccessible(true); $migrationsProperty->setValue($this->manager, $migrations); $this->manager->setEnvironment($envStub); $this->manager->migrate(null, false, $count); + $output = $this->getOutput(); if (is_null($expectedMigration)) { $this->assertEmpty($output, $message); @@ -776,7 +765,7 @@ public function testMigrationsByCount(array $availableMigrations, $count, $expec * migration to point to. */ #[DataProvider('rollbackToVersionDataProvider')] - public function testRollbackToVersion($availableRollbacks, $version, $expectedOutput) + public function testRollbackToVersion(array $availableRollbacks, ?string $version, string|array|null $expectedOutput): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -788,6 +777,7 @@ public function testRollbackToVersion($availableRollbacks, $version, $expectedOu $this->manager->setEnvironment($envStub); $this->manager->rollback($version); + $output = $this->getOutput(); if (is_null($expectedOutput)) { $output = explode("\n", $output); @@ -808,7 +798,7 @@ public function testRollbackToVersion($availableRollbacks, $version, $expectedOu * migration to point to. */ #[DataProvider('rollbackToDateDataProvider')] - public function testRollbackToDate($availableRollbacks, $version, $expectedOutput) + public function testRollbackToDate(array $availableRollbacks, string $version, string|array|null $expectedOutput): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -820,6 +810,7 @@ public function testRollbackToDate($availableRollbacks, $version, $expectedOutpu $this->manager->setEnvironment($envStub); $this->manager->rollback($version, false, false); + $output = $this->getOutput(); if (is_null($expectedOutput)) { $output = explode("\n", $output); @@ -839,7 +830,7 @@ public function testRollbackToDate($availableRollbacks, $version, $expectedOutpu * Test that rollbacking with count stops at the right migration. */ #[DataProvider('rollbackByCountDataProvider')] - public function testRollbackByCount($availableRollbacks, $count, $expectedOutput) + public function testRollbackByCount(array $availableRollbacks, int $count, string|array|null $expectedOutput): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -852,7 +843,7 @@ public function testRollbackByCount($availableRollbacks, $count, $expectedOutput // Mock getMigrations to return migrations matching the version log $migrations = []; foreach ($availableRollbacks as $version => $details) { - $migration = $this->getMockBuilder('\Migrations\MigrationInterface') + $migration = $this->getMockBuilder(MigrationInterface::class) ->getMock(); $migration->expects($this->any()) ->method('getVersion') @@ -869,11 +860,11 @@ public function testRollbackByCount($availableRollbacks, $count, $expectedOutput // Use reflection to set the migrations property $reflection = new ReflectionClass($this->manager); $migrationsProperty = $reflection->getProperty('migrations'); - $migrationsProperty->setAccessible(true); $migrationsProperty->setValue($this->manager, $migrations); $this->manager->setEnvironment($envStub); $this->manager->rollbackByCount($count); + $output = $this->getOutput(); if (is_null($expectedOutput)) { $output = explode("\n", $output); @@ -894,7 +885,7 @@ public function testRollbackByCount($availableRollbacks, $count, $expectedOutput * migration to point to. */ #[DataProvider('rollbackToVersionByExecutionTimeDataProvider')] - public function testRollbackToVersionByExecutionTime($availableRollbacks, $version, $expectedOutput) + public function testRollbackToVersionByExecutionTime(array $availableRollbacks, ?string $version, string|array $expectedOutput): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -912,6 +903,7 @@ public function testRollbackToVersionByExecutionTime($availableRollbacks, $versi $this->manager = new Manager($config, $this->io); $this->manager->setEnvironment($envStub); $this->manager->rollback($version); + $output = $this->getOutput(); if (is_null($expectedOutput)) { @@ -932,7 +924,7 @@ public function testRollbackToVersionByExecutionTime($availableRollbacks, $versi * migration to point to. */ #[DataProvider('rollbackToVersionByExecutionTimeDataProvider')] - public function testRollbackToVersionByName($availableRollbacks, $version, $expectedOutput) + public function testRollbackToVersionByName(array $availableRollbacks, ?string $version, string|array $expectedOutput): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -950,6 +942,7 @@ public function testRollbackToVersionByName($availableRollbacks, $version, $expe $this->manager = new Manager($config, $this->io); $this->manager->setEnvironment($envStub); $this->manager->rollback($availableRollbacks[$version]['migration_name'] ?? $version); + $output = $this->getOutput(); if (is_null($expectedOutput)) { @@ -970,7 +963,7 @@ public function testRollbackToVersionByName($availableRollbacks, $version, $expe * migration to point to. */ #[DataProvider('rollbackToDateByExecutionTimeDataProvider')] - public function testRollbackToDateByExecutionTime($availableRollbacks, $date, $expectedOutput) + public function testRollbackToDateByExecutionTime(array $availableRollbacks, ?string $date, string|array|null $expectedOutput): void { // stub environment $envStub = $this->getMockBuilder(Environment::class) @@ -988,6 +981,7 @@ public function testRollbackToDateByExecutionTime($availableRollbacks, $date, $e $this->manager = new Manager($config, $this->io); $this->manager->setEnvironment($envStub); $this->manager->rollback($date, false, false); + $output = $this->getOutput(); if (is_null($expectedOutput)) { @@ -1021,6 +1015,7 @@ public function testRollbackToVersionWithSingleMigrationDoesNotFail(): void $this->manager->setEnvironment($envStub); $this->manager->rollback(); + $output = $this->getOutput(); $this->assertStringContainsString('== 20120111235330 TestMigration: reverting', $output); @@ -1054,6 +1049,7 @@ public function testRollbackToVersionWithTwoMigrations(): void $this->manager->setEnvironment($envStub); $this->manager->rollback(); + $output = $this->getOutput(); $this->assertStringNotContainsString('== 20120111235330 TestMigration: reverting', $output); @@ -1080,7 +1076,8 @@ public function testRollbackLast(array $availableRolbacks, string $versionOrder, $this->manager = new Manager($config, $this->io); $this->manager->setEnvironment($envStub); - $this->manager->rollback(null); + $this->manager->rollback(); + $output = $this->getOutput(); if (is_null($expectedOutput)) { $this->assertEquals('No migrations to rollback' . PHP_EOL, $output); @@ -2425,7 +2422,7 @@ public function testExecuteSeedWorksAsExpected(): void $this->manager->setEnvironment($envStub); $this->manager->seed(); - $output = join("\n", $this->out->messages()); + $output = implode("\n", $this->out->messages()); $this->assertStringContainsString('GSeeder', $output); $this->assertStringContainsString('PostSeeder', $output); $this->assertStringContainsString('UserSeeder', $output); @@ -2439,7 +2436,8 @@ public function testExecuteASingleSeedWorksAsExpected(): void ->getMock(); $this->manager->setEnvironment($envStub); $this->manager->seed('UserSeeder'); - $output = join("\n", $this->out->messages()); + + $output = implode("\n", $this->out->messages()); $this->assertStringContainsString('UserSeeder', $output); } @@ -2474,7 +2472,7 @@ public function testSeedWillNotBeExecuted(): void $this->manager->setEnvironment($envStub); $this->manager->seed('UserSeederNotExecuted'); - $output = join("\n", $this->out->messages()); + $output = implode("\n", $this->out->messages()); $this->assertStringContainsString('skipped', $output); } @@ -2962,7 +2960,7 @@ public function testInvalidVersionBreakpoint(): void $this->manager->setEnvironment($envStub); $this->manager->setBreakpoint(20120133235330); - $output = join("\n", $this->out->messages()); + $output = implode("\n", $this->out->messages()); $this->assertStringContainsString('warning 20120133235330 is not a valid version', $output); } diff --git a/tests/TestCase/MigrationsTest.php b/tests/TestCase/MigrationsTest.php index 2830b88c2..44bc6c4af 100644 --- a/tests/TestCase/MigrationsTest.php +++ b/tests/TestCase/MigrationsTest.php @@ -55,7 +55,7 @@ class MigrationsTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -108,7 +108,7 @@ public function setUp(): void * * @return void */ - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); unset($this->Connection, $this->migrations); @@ -125,7 +125,7 @@ public function tearDown(): void * * @return void */ - public function testStatus() + public function testStatus(): void { $result = $this->migrations->status(); $expected = [ @@ -158,7 +158,7 @@ public function testStatus() * * @return void */ - public function testMigrateAndRollback() + public function testMigrateAndRollback(): void { if ($this->Connection->getDriver() instanceof Sqlserver) { // TODO This test currently fails in CI because numbers table @@ -252,7 +252,7 @@ public function testMigrateAndRollback() * * @return void */ - public function testCreateWithEncoding() + public function testCreateWithEncoding(): void { $this->skipIf(env('DB') !== 'mysql', 'Requires MySQL'); @@ -279,7 +279,7 @@ public function testCreateWithEncoding() * * @return void */ - public function testMarkMigratedAll() + public function testMarkMigratedAll(): void { $markMigrated = $this->migrations->markMigrated(); $this->assertTrue($markMigrated); @@ -316,7 +316,7 @@ public function testMarkMigratedAll() * * @return void */ - public function testMarkMigratedAllAsVersion() + public function testMarkMigratedAllAsVersion(): void { $markMigrated = $this->migrations->markMigrated('all'); $this->assertTrue($markMigrated); @@ -352,7 +352,7 @@ public function testMarkMigratedAllAsVersion() * * @return void */ - public function testMarkMigratedTarget() + public function testMarkMigratedTarget(): void { $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200']); $this->assertTrue($markMigrated); @@ -394,7 +394,7 @@ public function testMarkMigratedTarget() * * @return void */ - public function testMarkMigratedTargetError() + public function testMarkMigratedTargetError(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Migration `20150704160610` was not found !'); @@ -407,7 +407,7 @@ public function testMarkMigratedTargetError() * * @return void */ - public function testMarkMigratedTargetExclude() + public function testMarkMigratedTargetExclude(): void { $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150704160200', 'exclude' => true]); $this->assertTrue($markMigrated); @@ -449,7 +449,7 @@ public function testMarkMigratedTargetExclude() * * @return void */ - public function testMarkMigratedTargetOnly() + public function testMarkMigratedTargetOnly(): void { $markMigrated = $this->migrations->markMigrated(null, ['target' => '20150724233100', 'only' => true]); $this->assertTrue($markMigrated); @@ -491,7 +491,7 @@ public function testMarkMigratedTargetOnly() * * @return void */ - public function testMarkMigratedTargetExcludeOnly() + public function testMarkMigratedTargetExcludeOnly(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('You should use `exclude` OR `only` (not both) along with a `target` argument'); @@ -504,7 +504,7 @@ public function testMarkMigratedTargetExcludeOnly() * * @return void */ - public function testMarkMigratedVersion() + public function testMarkMigratedVersion(): void { $markMigrated = $this->migrations->markMigrated(20150704160200); $this->assertTrue($markMigrated); @@ -546,7 +546,7 @@ public function testMarkMigratedVersion() * * @return void */ - public function testOverrideOptions() + public function testOverrideOptions(): void { $result = $this->migrations->status(); $expectedStatus = [ @@ -614,7 +614,7 @@ public function testOverrideOptions() * * @return void */ - public function testMigrateDateOption() + public function testMigrateDateOption(): void { // If we want to migrate to a date before the first first migration date, // we should not migrate anything @@ -674,6 +674,7 @@ public function testMigrateDateOption() // If we want to migrate to a date after the last migration date, // we should migrate everything $this->migrations->migrate(['date' => '20150730']); + $expectedStatus = [ [ 'status' => 'up', @@ -790,7 +791,7 @@ public function testMigrateDateOption() * * @return void */ - public function testSeed() + public function testSeed(): void { $this->migrations->migrate(); $seed = $this->migrations->seed(['source' => 'Seeds']); @@ -866,7 +867,7 @@ public function testSeed() * * @return void */ - public function testSeedOneSeeder() + public function testSeedOneSeeder(): void { $this->migrations->migrate(); @@ -915,7 +916,7 @@ public function testSeedOneSeeder() * * @return void */ - public function testSeedOneSeederShortName() + public function testSeedOneSeederShortName(): void { $this->migrations->migrate(); @@ -964,7 +965,7 @@ public function testSeedOneSeederShortName() * * @return void */ - public function testSeedCallSeeder() + public function testSeedCallSeeder(): void { $this->migrations->migrate(); @@ -1025,7 +1026,7 @@ public function testSeedCallSeeder() * * @return void */ - public function testSeedWrongSeed() + public function testSeedWrongSeed(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The seed `DerpSeed` does not exist'); @@ -1086,7 +1087,7 @@ public function testMigrateSnapshots(string $basePath, string $filename, array $ 'parts', 'special_pks', 'special_tags', 'texts', 'users', ]; foreach ($cleanup as $item) { - $snapshotConn->execute("DROP TABLE IF EXISTS {$item}"); + $snapshotConn->execute('DROP TABLE IF EXISTS ' . $item); } $migrations = new Migrations([ @@ -1103,7 +1104,7 @@ public function testMigrateSnapshots(string $basePath, string $filename, array $ /** * Tests that migrating in case of error throws an exception */ - public function testMigrateErrors() + public function testMigrateErrors(): void { $this->expectException(Exception::class); $this->migrations->markMigrated(20150704160200); @@ -1113,7 +1114,7 @@ public function testMigrateErrors() /** * Tests that rolling back in case of error throws an exception */ - public function testRollbackErrors() + public function testRollbackErrors(): void { $this->expectException(Exception::class); $this->migrations->markMigrated('all'); @@ -1124,7 +1125,7 @@ public function testRollbackErrors() * Tests that marking migrated a non-existant migrations returns an error * and can return a error message */ - public function testMarkMigratedErrors() + public function testMarkMigratedErrors(): void { $this->expectException(Exception::class); $this->migrations->markMigrated(20150704000000); diff --git a/tests/TestCase/TestCase.php b/tests/TestCase/TestCase.php index 06b0f68ec..e9734f79c 100644 --- a/tests/TestCase/TestCase.php +++ b/tests/TestCase/TestCase.php @@ -39,7 +39,7 @@ abstract class TestCase extends BaseTestCase */ protected $generatedFiles = []; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -47,7 +47,7 @@ public function setUp(): void $this->loadPlugins(['Cake/TwigView']); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); @@ -72,9 +72,9 @@ public function tearDown(): void * @param string $name plugin name to load * @return void */ - protected function _loadTestPlugin($name) + protected function _loadTestPlugin(string $name) { - $root = dirname(dirname(__FILE__)) . DS; + $root = dirname(__FILE__, 2) . DS; $path = $root . 'test_app' . DS . 'Plugin' . DS . $name . DS; $this->loadPlugins([ @@ -91,7 +91,7 @@ protected function _loadTestPlugin($name) * @param string $message The message to use if a check fails. * @return void */ - protected function assertFilesExist(array $files, $message = '') + protected function assertFilesExist(array $files, string $message = '') { foreach ($files as $file) { $this->assertFileExists($file, $message); @@ -106,7 +106,7 @@ protected function assertFilesExist(array $files, $message = '') * @param string $message The error message. * @return void */ - protected function assertFileContains($expected, $path, $message = '') + protected function assertFileContains(string $expected, string $path, string $message = '') { $this->assertFileExists($path, 'Cannot test contents, file does not exist.'); @@ -122,7 +122,7 @@ protected function assertFileContains($expected, $path, $message = '') * @param string $message The error message. * @return void */ - protected function assertFileNotContains($expected, $path, $message = '') + protected function assertFileNotContains(string $expected, string $path, string $message = '') { $this->assertFileExists($path, 'Cannot test contents, file does not exist.'); diff --git a/tests/TestCase/TestSuite/MigratorTest.php b/tests/TestCase/TestSuite/MigratorTest.php index de3baa975..9363dba40 100644 --- a/tests/TestCase/TestSuite/MigratorTest.php +++ b/tests/TestCase/TestSuite/MigratorTest.php @@ -55,7 +55,7 @@ protected function getMigratorWhereClause(): array : []; } - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -67,7 +67,7 @@ public function setUp(): void (new ConnectionHelper())->dropTables('test'); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); if ($this->restore) { @@ -185,7 +185,7 @@ public function testTruncateAfterMigrations(): void $this->assertCount(0, $connection->selectQuery()->select(['*'])->from('migrator')->execute()->fetchAll()); } - private function setMigrationEndDateToYesterday() + private function setMigrationEndDateToYesterday(): void { $query = ConnectionManager::get('test')->updateQuery() ->update($this->getMigratorTableName()) diff --git a/tests/TestCase/Util/ColumnParserTest.php b/tests/TestCase/Util/ColumnParserTest.php index d56d0616a..5fddc4799 100644 --- a/tests/TestCase/Util/ColumnParserTest.php +++ b/tests/TestCase/Util/ColumnParserTest.php @@ -31,13 +31,13 @@ class ColumnParserTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); $this->columnParser = new ColumnParser(); } - public function testParseFields() + public function testParseFields(): void { $this->assertEquals([ 'id' => [ @@ -227,7 +227,7 @@ public function testParseFields() $this->assertEquals($expected, $actual); } - public function testParseIndexes() + public function testParseIndexes(): void { $this->assertEquals(['UNIQUE_ID' => [ 'columns' => ['id'], @@ -247,7 +247,7 @@ public function testParseIndexes() ])); } - public function testParsePrimaryKey() + public function testParsePrimaryKey(): void { $this->assertEquals(['id'], $this->columnParser->parsePrimaryKey(['id:primary'])); $this->assertEquals(['id'], $this->columnParser->parsePrimaryKey(['id:integer:primary'])); @@ -258,7 +258,7 @@ public function testParsePrimaryKey() ); } - public function testValidArguments() + public function testValidArguments(): void { $this->assertEquals( ['id'], @@ -310,7 +310,7 @@ public function testValidArguments() ); } - public function testGetType() + public function testGetType(): void { $this->assertSame('integer', $this->columnParser->getType('id', null)); $this->assertSame('integer', $this->columnParser->getType('id', 'primary_key')); @@ -332,7 +332,7 @@ public function testGetType() $this->assertSame('decimal', $this->columnParser->getType('longitude', null)); } - public function testGetTypeAndLength() + public function testGetTypeAndLength(): void { $this->assertEquals(['string', 255], $this->columnParser->getTypeAndLength('name', 'string')); $this->assertEquals(['integer', 11], $this->columnParser->getTypeAndLength('counter', 'integer')); @@ -346,7 +346,7 @@ public function testGetTypeAndLength() $this->assertEquals(['decimal', [10, 6]], $this->columnParser->getTypeAndLength('latitude', 'decimal[10,6]')); } - public function testGetTypeAndLengthReturnsIntegerTypes() + public function testGetTypeAndLengthReturnsIntegerTypes(): void { // Test that lengths are returned as integers, not strings [, $length] = $this->columnParser->getTypeAndLength('name', 'string[128]'); @@ -376,7 +376,7 @@ public function testGetTypeAndLengthReturnsIntegerTypes() $this->assertSame(11, $length); } - public function testGetLength() + public function testGetLength(): void { $this->assertSame(255, $this->columnParser->getLength('string')); $this->assertSame(11, $this->columnParser->getLength('integer')); @@ -385,7 +385,7 @@ public function testGetLength() $this->assertNull($this->columnParser->getLength('text')); } - public function testGetIndexName() + public function testGetIndexName(): void { $this->assertSame('SOME_INDEX', $this->columnParser->getIndexName('id', null, 'SOME_INDEX', true)); $this->assertSame('SOME_INDEX', $this->columnParser->getIndexName('id', null, 'SOME_INDEX', false)); @@ -398,7 +398,7 @@ public function testGetIndexName() $this->assertSame('PRIMARY', $this->columnParser->getIndexName('id', 'primary', null, true)); } - public function testParseFieldsWithReferences() + public function testParseFieldsWithReferences(): void { // Test basic references - should convert to integer $expected = [ @@ -443,7 +443,7 @@ public function testParseFieldsWithReferences() $this->assertEquals($expected, $actual); } - public function testParseFieldsWithDefaultValues() + public function testParseFieldsWithDefaultValues(): void { // Test boolean default true $expected = [ @@ -557,7 +557,7 @@ public function testParseFieldsWithDefaultValues() $this->assertEquals($expected, $actual); } - public function testParseDefaultValue() + public function testParseDefaultValue(): void { // Test null and empty values $this->assertNull($this->columnParser->parseDefaultValue(null, 'string')); @@ -588,7 +588,7 @@ public function testParseDefaultValue() $this->assertSame('CURRENT_TIMESTAMP', $this->columnParser->parseDefaultValue('CURRENT_TIMESTAMP', 'datetime')); } - public function testParseIndexesWithDefaultValues() + public function testParseIndexesWithDefaultValues(): void { // Ensure indexes still work with default values in the definition $expected = [ @@ -611,7 +611,7 @@ public function testParseIndexesWithDefaultValues() $this->assertEquals($expected, $actual); } - public function testValidArgumentsWithDefaultValues() + public function testValidArgumentsWithDefaultValues(): void { $this->assertEquals( ['active:boolean:default[true]'], @@ -627,7 +627,7 @@ public function testValidArgumentsWithDefaultValues() ); } - public function testParseForeignKeys() + public function testParseForeignKeys(): void { // Test basic reference - infer table name from field $expected = [ diff --git a/tests/TestCase/Util/UtilTest.php b/tests/TestCase/Util/UtilTest.php index c017c7641..6c9aed720 100644 --- a/tests/TestCase/Util/UtilTest.php +++ b/tests/TestCase/Util/UtilTest.php @@ -12,12 +12,12 @@ class UtilTest extends TestCase { - private function getCorrectedPath($path) + private function getCorrectedPath(string $path): string { return str_replace('/', DIRECTORY_SEPARATOR, $path); } - public function testGetExistingMigrationClassNames() + public function testGetExistingMigrationClassNames(): void { $expectedResults = [ 'TestMigration', @@ -31,14 +31,14 @@ public function testGetExistingMigrationClassNames() } } - public function testGetExistingMigrationClassNamesWithFile() + public function testGetExistingMigrationClassNamesWithFile(): void { $file = $this->getCorrectedPath(__DIR__ . '/_files/migrations/20120111235330_test_migration.php'); $existingClassNames = Util::getExistingMigrationClassNames($file); $this->assertCount(0, $existingClassNames); } - public function testGetCurrentTimestamp() + public function testGetCurrentTimestamp(): void { $dt = new DateTime('now', new DateTimeZone('UTC')); $expected = $dt->format(Util::DATE_FORMAT); @@ -104,7 +104,7 @@ public static function providerMapFileName(): array } #[DataProvider('providerMapFileName')] - public function testMapFileNameToClassName(string $fileName, string $className) + public function testMapFileNameToClassName(string $fileName, string $className): void { $this->assertEquals($className, Util::mapFileNameToClassName($fileName)); } @@ -117,7 +117,7 @@ public function testMapReadableFileNameToClassName(): void $this->assertEquals('DropOrdersTable', Util::mapFileNameToClassName('2024_01_01_000000_DropOrdersTable.php')); } - public function testGlobPath() + public function testGlobPath(): void { $files = Util::glob(__DIR__ . '/_files/migrations/empty.txt'); $this->assertCount(1, $files); @@ -130,7 +130,7 @@ public function testGlobPath() $this->assertEquals('not_a_migration.php', basename($files[2])); } - public function testGlobAll() + public function testGlobAll(): void { $files = Util::globAll([ __DIR__ . '/_files/migrations/*.php', @@ -144,7 +144,7 @@ public function testGlobAll() $this->assertEquals('empty.txt', basename($files[3])); } - public function testGetFiles() + public function testGetFiles(): void { $files = Util::getFiles([ __DIR__ . '/_files/migrations', diff --git a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php index fd8abe276..69edb0937 100644 --- a/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php +++ b/tests/TestCase/Util/_files/migrations/20120111235330_test_migration.php @@ -8,7 +8,7 @@ class TestMigration extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -16,7 +16,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/TestCase/View/Helper/MigrationHelperTest.php b/tests/TestCase/View/Helper/MigrationHelperTest.php index 0f64e4dd0..ad1a5073b 100644 --- a/tests/TestCase/View/Helper/MigrationHelperTest.php +++ b/tests/TestCase/View/Helper/MigrationHelperTest.php @@ -76,7 +76,7 @@ class MigrationHelperTest extends TestCase * * @return void */ - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -141,34 +141,34 @@ public function setUp(): void * * @return void */ - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); unset($this->helper, $this->view, $this->collection, $this->connection); } - public function testTableMethod() + public function testTableMethod(): void { $this->assertSame('drop', $this->helper->tableMethod('drop_table')); $this->assertSame('create', $this->helper->tableMethod('create_table')); $this->assertSame('update', $this->helper->tableMethod('other_method')); } - public function testIndexMethod() + public function testIndexMethod(): void { $this->assertSame('removeIndex', $this->helper->indexMethod('drop_field')); $this->assertSame('addIndex', $this->helper->indexMethod('add_field')); $this->assertSame('addIndex', $this->helper->indexMethod('alter_field')); } - public function testColumnMethod() + public function testColumnMethod(): void { $this->assertSame('removeColumn', $this->helper->columnMethod('drop_field')); $this->assertSame('addColumn', $this->helper->columnMethod('add_field')); $this->assertSame('changeColumn', $this->helper->columnMethod('alter_field')); } - public function testColumns() + public function testColumns(): void { $extra = []; if ($this->connection->getDriver() instanceof Sqlserver) { @@ -218,7 +218,7 @@ public function testColumns() ], $this->helper->columns('users')); } - public function testColumn() + public function testColumn(): void { $tableSchema = $this->collection->describe('users'); @@ -289,7 +289,7 @@ public function testColumn() ], $this->helper->column($tableSchema, 'updated')); } - public function testValue() + public function testValue(): void { $this->assertSame('null', $this->helper->value(null)); $this->assertSame('null', $this->helper->value('null')); @@ -309,7 +309,7 @@ public function testValue() $this->assertSame("'o\\\"ne'", $this->helper->value('o"ne')); } - public function testAttributes() + public function testAttributes(): void { $attributes = [ 'null' => false, @@ -381,7 +381,7 @@ public function testAttributes() $this->assertEquals($attributes, $result); } - public function testStringifyList() + public function testStringifyList(): void { $this->assertSame('', $this->helper->stringifyList([])); $this->assertSame(" diff --git a/tests/bootstrap.php b/tests/bootstrap.php index cf4caad6f..f92d9157b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -24,10 +24,10 @@ use Migrator\MigratorPlugin; use function Cake\Core\env; -$findRoot = function ($root) { +$findRoot = function ($root): string { do { $lastRoot = $root; - $root = dirname($root); + $root = dirname((string)$root); if (is_dir($root . '/vendor/cakephp/cakephp')) { return $root; } @@ -38,7 +38,7 @@ unset($findRoot); chdir($root); -require_once 'vendor/autoload.php'; +require_once __DIR__ . '/../vendor/autoload.php'; if (!defined('DS')) { define('DS', DIRECTORY_SEPARATOR); diff --git a/tests/test_app/App/Http/TestRequestHandler.php b/tests/test_app/App/Http/TestRequestHandler.php index 2b376a4f5..dfe37dce3 100644 --- a/tests/test_app/App/Http/TestRequestHandler.php +++ b/tests/test_app/App/Http/TestRequestHandler.php @@ -10,11 +10,14 @@ class TestRequestHandler implements RequestHandlerInterface { + /** + * @var callable + */ public $callable; public function __construct(?callable $callable = null) { - $this->callable = $callable ?: function ($request) { + $this->callable = $callable ?: function ($request): Response { return new Response(); }; } diff --git a/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php b/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php index abc0aaa4f..c3051a35c 100644 --- a/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php +++ b/tests/test_app/Plugin/Migrator/config/Migrations/20211001000000_migrator.php @@ -4,7 +4,7 @@ class Migrator extends BaseMigration { - public function up() + public function up(): void { $this->table('migrator')->addColumn('test', 'integer')->create(); $this->table('migrator')->insert(['test' => 1])->save(); diff --git a/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php b/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php index e06b74823..8cb055972 100644 --- a/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php +++ b/tests/test_app/Plugin/Migrator/config/Migrations2/20211002000000_migrator2.php @@ -4,7 +4,7 @@ class Migrator2 extends BaseMigration { - public function up() + public function up(): void { $this->table('migrator')->insert(['test' => 2])->save(); } diff --git a/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php b/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php index a24c27de9..8f274973f 100644 --- a/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php +++ b/tests/test_app/config/DropColumnFkIndexRegression/20190928205056_first_fk_index_migration.php @@ -4,7 +4,7 @@ class FirstFkIndexMigration extends BaseMigration { - public function up() + public function up(): void { $this->table('table2', [ 'id' => false, @@ -87,7 +87,7 @@ public function up() ->create(); } - public function down() + public function down(): void { $this->table('table1', [ 'id' => false, diff --git a/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php b/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php index f6c7cb22e..2835b5b9f 100644 --- a/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php +++ b/tests/test_app/config/DropColumnFkIndexRegression/20190928205060_second_fk_index_migration.php @@ -4,7 +4,7 @@ class SecondFkIndexMigration extends BaseMigration { - public function up() + public function up(): void { $this->table('table1', [ 'id' => false, diff --git a/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php b/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php index 88f18e47d..f173b072a 100644 --- a/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php +++ b/tests/test_app/config/DropIndexRegression/20121213232502_create_drop_fk_initial_schema.php @@ -7,7 +7,7 @@ class CreateDropFkInitialSchema extends BaseMigration /** * Change. */ - public function change() + public function change(): void { $this->table('my_table') ->addColumn('name', 'string') diff --git a/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php b/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php index 4b1a5a7c9..72f8d3827 100644 --- a/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php +++ b/tests/test_app/config/DropIndexRegression/20121223011815_add_regression_drop_fk.php @@ -7,7 +7,7 @@ class AddRegressionDropFk extends BaseMigration /** * Change. */ - public function change() + public function change(): void { $table = $this->table('my_table'); $table diff --git a/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php b/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php index b3ec6f21b..020e82b01 100644 --- a/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php +++ b/tests/test_app/config/DropIndexRegression/20121223011816_change_fk_regression.php @@ -7,7 +7,7 @@ class ChangeFkRegression extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { $table = $this->table('my_table'); $table diff --git a/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php b/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php index abbd5a8ac..f95ce31e8 100644 --- a/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php +++ b/tests/test_app/config/DropIndexRegression/20121223011817_change_column_regression.php @@ -7,7 +7,7 @@ class ChangeColumnRegression extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { $table = $this->table('my_table'); $table diff --git a/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php b/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php index 376669a65..36f4be4a4 100644 --- a/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php +++ b/tests/test_app/config/DropTableWithFkRegression/20190928205056_first_drop_fk_migration.php @@ -4,7 +4,7 @@ class FirstDropFkMigration extends BaseMigration { - public function change() + public function change(): void { $this->table('orders') ->addColumn('order_date', 'timestamp') diff --git a/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php b/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php index 2fc464337..f1161cb41 100644 --- a/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php +++ b/tests/test_app/config/DropTableWithFkRegression/20190928205060_second_drop_fk_migration.php @@ -4,7 +4,7 @@ class SecondDropFkMigration extends BaseMigration { - public function change() + public function change(): void { $this->table('customers') ->addColumn('name', 'text') diff --git a/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php b/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php index 4a8dddeb4..d133d6c04 100644 --- a/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php +++ b/tests/test_app/config/Duplicatenames/20120111235330_duplicate_migration_name.php @@ -7,7 +7,7 @@ class DuplicateMigrationName extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php b/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php index 4a8dddeb4..d133d6c04 100644 --- a/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php +++ b/tests/test_app/config/Duplicatenames/20120111235331_duplicate_migration_name.php @@ -7,7 +7,7 @@ class DuplicateMigrationName extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php index 159962d0a..7f9a90ba4 100644 --- a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php +++ b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration.php @@ -7,7 +7,7 @@ class DuplicateMigration extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php index 4f548649c..7e0bce59d 100644 --- a/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php +++ b/tests/test_app/config/Duplicateversions/20120111235330_duplicate_migration_2.php @@ -7,7 +7,7 @@ class DuplicateMigration2 extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php b/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php index b147e2f92..942b856b6 100644 --- a/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php +++ b/tests/test_app/config/Invalidclassname/20120111235330_invalid_class.php @@ -7,7 +7,7 @@ class DifferentClassName extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/ManagerMigrations/20120111235330_test_migration.php b/tests/test_app/config/ManagerMigrations/20120111235330_test_migration.php index 8a4180048..863a9f375 100644 --- a/tests/test_app/config/ManagerMigrations/20120111235330_test_migration.php +++ b/tests/test_app/config/ManagerMigrations/20120111235330_test_migration.php @@ -7,7 +7,7 @@ class TestMigration extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/ManagerMigrations/20120116183504_test_migration_2.php b/tests/test_app/config/ManagerMigrations/20120116183504_test_migration_2.php index 70d0b0e06..c98b7fbf9 100644 --- a/tests/test_app/config/ManagerMigrations/20120116183504_test_migration_2.php +++ b/tests/test_app/config/ManagerMigrations/20120116183504_test_migration_2.php @@ -7,7 +7,7 @@ class TestMigration2 extends BaseMigration /** * Migrate Up. */ - public function up() + public function up(): void { // do nothing } @@ -15,7 +15,7 @@ public function up() /** * Migrate Down. */ - public function down() + public function down(): void { // do nothing } diff --git a/tests/test_app/config/Postgres/20180516025208_snapshot_pgsql.php b/tests/test_app/config/Postgres/20180516025208_snapshot_pgsql.php index 843c8fafb..e221f3368 100644 --- a/tests/test_app/config/Postgres/20180516025208_snapshot_pgsql.php +++ b/tests/test_app/config/Postgres/20180516025208_snapshot_pgsql.php @@ -4,7 +4,7 @@ class SnapshotPgsql extends BaseMigration { - public function up() + public function up(): void { $this->table('articles') ->addColumn('title', 'string', [ @@ -304,7 +304,7 @@ public function up() ->update(); } - public function down() + public function down(): void { $this->table('articles') ->dropForeignKey( diff --git a/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php b/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php index f27456ea0..4f94bfd69 100644 --- a/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php +++ b/tests/test_app/config/Reversiblemigrations/20121213232502_create_initial_schema.php @@ -8,7 +8,7 @@ class CreateInitialSchema extends BaseMigration /** * Change. */ - public function change() + public function change(): void { // users table $users = $this->table('users'); diff --git a/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php b/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php index 147fdfcf7..3b2a15e9f 100644 --- a/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php +++ b/tests/test_app/config/Reversiblemigrations/20121223011815_update_info_table.php @@ -8,7 +8,7 @@ class UpdateInfoTable extends BaseMigration /** * Change. */ - public function change() + public function change(): void { // info table $info = $this->table('info'); diff --git a/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php b/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php index 767a0f9c6..5e49f0383 100644 --- a/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php +++ b/tests/test_app/config/Reversiblemigrations/20121224200649_rename_info_table_to_statuses_table.php @@ -7,7 +7,7 @@ class RenameInfoTableToStatusesTable extends BaseMigration /** * Change. */ - public function change() + public function change(): void { // users table $table = $this->table('info'); diff --git a/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php b/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php index 42423bafc..3e91fa180 100644 --- a/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php +++ b/tests/test_app/config/Reversiblemigrations/20121224200739_rename_bio_to_biography.php @@ -7,7 +7,7 @@ class RenameBioToBiography extends BaseMigration /** * Change. */ - public function change() + public function change(): void { // users table $table = $this->table('users'); diff --git a/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php b/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php index 8e7ec6332..24dfdec76 100644 --- a/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php +++ b/tests/test_app/config/Reversiblemigrations/20121224200852_create_user_logins_table.php @@ -8,7 +8,7 @@ class CreateUserLoginsTable extends BaseMigration /** * Change. */ - public function change() + public function change(): void { // user logins table $table = $this->table('user_logins'); diff --git a/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php b/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php index d87da33d2..12b49727e 100644 --- a/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php +++ b/tests/test_app/config/Reversiblemigrations/20170824134305_direction_aware_reversible_up.php @@ -5,7 +5,7 @@ class DirectionAwareReversibleUp extends BaseMigration { - public function change() + public function change(): void { $this->table('change_direction_test') ->addColumn('thing', Column::STRING, [ diff --git a/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php b/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php index 3455b2ded..3229cd498 100644 --- a/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php +++ b/tests/test_app/config/Reversiblemigrations/20170831121929_direction_aware_reversible_down.php @@ -6,7 +6,7 @@ class DirectionAwareReversibleDown extends BaseMigration { - public function change() + public function change(): void { $this->table('change_direction_test') ->addColumn('subthing', Column::STRING, [ diff --git a/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php b/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php index fdd8f32d6..00a80f3ba 100644 --- a/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php +++ b/tests/test_app/config/Reversiblemigrations/20180431121930_tricky_edge_case.php @@ -5,7 +5,7 @@ class TrickyEdgeCase extends BaseMigration { - public function change() + public function change(): void { $table = $this->table('user_logins'); $table diff --git a/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php b/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php index d118bed6e..5af15e185 100644 --- a/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php +++ b/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php @@ -26,7 +26,7 @@ class CreateTestIndexLimitSpecifierTable extends BaseMigration * Remember to call "create()" or "update()" and NOT "save()" when working * with the Table class. */ - public function change() + public function change(): void { $table = $this->table('test_index_limit_specifier'); $table->addColumn('column1', Column::STRING) diff --git a/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php b/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php index 74b4b3238..45f3dad78 100644 --- a/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php +++ b/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php @@ -31,7 +31,7 @@ class AddColumnIndexFk extends BaseMigration * Remember to call "create()" or "update()" and NOT "save()" when working * with the Table class. */ - public function change() + public function change(): void { $table = $this->table('statuses') ->addColumn('user_id', Column::INTEGER, [ diff --git a/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php b/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php index 1e45f604e..d6c4da16e 100644 --- a/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php +++ b/tests/test_app/config/ShouldExecute/20201207205056_should_not_execute_migration.php @@ -5,7 +5,7 @@ class ShouldNotExecuteMigration extends BaseMigration { - public function change() + public function change(): void { // info table $this->table('info')->create(); diff --git a/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php b/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php index 7e34549b3..d35a45edd 100644 --- a/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php +++ b/tests/test_app/config/ShouldExecute/20201207205057_should_execute_migration.php @@ -5,7 +5,7 @@ class ShouldExecuteMigration extends BaseMigration { - public function change() + public function change(): void { // info table $this->table('info')->create(); From 05084e25e83e9889909bd12a7f3fceeb4ca0c204 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 31 Mar 2026 04:18:03 +0200 Subject: [PATCH 26/72] Fix PHPStan error - getName() returns string not nullable --- src/Db/Action/ChangeColumn.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/Action/ChangeColumn.php b/src/Db/Action/ChangeColumn.php index cb2fa7192..a3770fb45 100644 --- a/src/Db/Action/ChangeColumn.php +++ b/src/Db/Action/ChangeColumn.php @@ -37,7 +37,7 @@ public function __construct(TableMetadata $table, string $columnName, Column $co $this->column = $column; // if the name was omitted use the existing column name - if ($column->getName() === null || $column->getName() === '') { + if ($column->getName() === '') { $column->setName($columnName); } } From f51320762f98e5c5060f04678b6eef6507d37dd3 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 31 Mar 2026 04:21:24 +0200 Subject: [PATCH 27/72] Apply rector fixes --- src/BaseSeed.php | 4 ++-- src/Command/BakeMigrationDiffCommand.php | 5 ++++- src/Command/ResetCommand.php | 10 +++++----- src/Db/Action/AddCheckConstraint.php | 2 -- src/Db/Action/DropCheckConstraint.php | 2 -- src/Db/Adapter/AbstractAdapter.php | 2 +- src/Db/Adapter/SqliteAdapter.php | 2 +- src/Db/Adapter/SqlserverAdapter.php | 2 +- src/Db/Table.php | 4 ++-- src/Util/TableFinder.php | 2 +- tests/TestCase/Command/ResetCommandTest.php | 6 +++--- tests/TestCase/Db/Table/TableTest.php | 2 +- 12 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/BaseSeed.php b/src/BaseSeed.php index 2126d1911..1ea4ab9d1 100644 --- a/src/BaseSeed.php +++ b/src/BaseSeed.php @@ -263,8 +263,8 @@ protected function runCall(string $seeder, array $options = []): void $options += [ 'connection' => $connection, - 'plugin' => $pluginName ?? ($config !== null ? $config['plugin'] : null), - 'source' => $config !== null ? $config['source'] : null, + 'plugin' => $pluginName ?? ($config instanceof \Migrations\Config\ConfigInterface ? $config['plugin'] : null), + 'source' => $config instanceof \Migrations\Config\ConfigInterface ? $config['source'] : null, ]; $factory = new ManagerFactory([ 'connection' => $options['connection'], diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index 816c1b94b..b9017941f 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -277,7 +277,10 @@ protected function getColumns(): void // changes in columns meta-data foreach ($currentColumns as $columnName) { $column = $this->safeGetColumn($currentSchema, $columnName); - if ($column === null || !in_array($columnName, $oldColumns, true)) { + if ($column === null) { + continue; + } + if (!in_array($columnName, $oldColumns, true)) { continue; } diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php index db0067356..a442eeaa6 100644 --- a/src/Command/ResetCommand.php +++ b/src/Command/ResetCommand.php @@ -55,7 +55,7 @@ public static function defaultName(): string * @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser->setDescription([ 'Drop all tables and re-run all migrations.', @@ -116,7 +116,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int // Get tables to drop $tablesToDrop = $this->getTablesToDrop($connection); - if (empty($tablesToDrop)) { + if ($tablesToDrop === []) { $io->out('No tables to drop.'); $io->out(''); $io->out('Running migrations...'); @@ -204,7 +204,7 @@ protected function dropTables(AdapterInterface $adapter, array $tables, ConsoleI try { foreach ($tables as $table) { - $io->verbose("Dropping table: {$table}"); + $io->verbose('Dropping table: ' . $table); $adapter->dropTable($table); } } finally { @@ -248,12 +248,12 @@ protected function runMigrations(Arguments $args, ConsoleIo $io): ?int $manager = $factory->createManager($io); $config = $manager->getConfig(); - $io->verbose('using connection ' . (string)$args->getOption('connection')); + $io->verbose('using connection ' . $args->getOption('connection')); $io->verbose('using paths ' . $config->getMigrationPath()); try { $start = microtime(true); - $manager->migrate(null, false, null); + $manager->migrate(null, false); $end = microtime(true); } catch (Throwable $e) { $io->err('' . $e->getMessage() . ''); diff --git a/src/Db/Action/AddCheckConstraint.php b/src/Db/Action/AddCheckConstraint.php index 81b2ebab4..ca558208e 100644 --- a/src/Db/Action/AddCheckConstraint.php +++ b/src/Db/Action/AddCheckConstraint.php @@ -15,8 +15,6 @@ class AddCheckConstraint extends Action { /** * The check constraint to add - * - * @var \Migrations\Db\Table\CheckConstraint */ protected CheckConstraint $checkConstraint; diff --git a/src/Db/Action/DropCheckConstraint.php b/src/Db/Action/DropCheckConstraint.php index 559cfb19e..85355dd74 100644 --- a/src/Db/Action/DropCheckConstraint.php +++ b/src/Db/Action/DropCheckConstraint.php @@ -14,8 +14,6 @@ class DropCheckConstraint extends Action { /** * The check constraint name to drop - * - * @var string */ protected string $constraintName; diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 8e7f66c50..903a43d68 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -194,7 +194,7 @@ public function getConnection(): Connection $this->connection = $this->getOption('connection'); $this->connect(); } - if ($this->connection === null) { + if (!$this->connection instanceof \Cake\Database\Connection) { throw new RuntimeException('Unable to establish database connection. Ensure a connection is configured.'); } diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index b1af4d20a..853c7ae46 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -1124,7 +1124,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column { $instructions = $this->beginAlterByCopyTable($tableName); - $newColumnName = (string)$newColumn->getName(); + $newColumnName = $newColumn->getName(); $instructions->addPostStep(function (array $state) use ($columnName, $newColumn): array { $dialect = $this->getSchemaDialect(); $sql = (string)preg_replace( diff --git a/src/Db/Adapter/SqlserverAdapter.php b/src/Db/Adapter/SqlserverAdapter.php index 35231994c..9d50c84f0 100644 --- a/src/Db/Adapter/SqlserverAdapter.php +++ b/src/Db/Adapter/SqlserverAdapter.php @@ -297,7 +297,7 @@ public function disableForeignKeyConstraints(): void foreach ($rows as $row) { $constraintName = $this->quoteColumnName($row['constraint_name']); $tableName = $this->quoteTableName($row['table_name']); - $this->execute("ALTER TABLE {$tableName} DROP CONSTRAINT {$constraintName}"); + $this->execute(sprintf('ALTER TABLE %s DROP CONSTRAINT %s', $tableName, $constraintName)); } } diff --git a/src/Db/Table.php b/src/Db/Table.php index 75590a85f..ee68e53d1 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -369,7 +369,7 @@ public function addColumn(string|Column $columnName, ?string $type = null, array throw new InvalidArgumentException(sprintf( 'An invalid column type "%s" was specified for column "%s".', $column->getType(), - (string)$column->getName(), + $column->getName(), )); } @@ -1186,7 +1186,7 @@ protected function executeActions(bool $exists): void } // Only skip CreateTable if we have ONLY view/trigger actions (and at least one) - if (!$hasViewOrTriggerActions || $hasTableActions || count($actions) === 0) { + if (!$hasViewOrTriggerActions || $hasTableActions || $actions === []) { $this->actions->addAction(new CreateTable($this->table)); } } diff --git a/src/Util/TableFinder.php b/src/Util/TableFinder.php index 800ed7dae..a894390aa 100644 --- a/src/Util/TableFinder.php +++ b/src/Util/TableFinder.php @@ -178,7 +178,7 @@ public function fetchTableName(string $className, ?string $pluginName = null): a $table = TableRegistry::getTableLocator()->get($className); foreach ($table->associations()->keys() as $key) { $association = $table->associations()->get($key); - if ($association !== null && $association->type() === 'belongsToMany') { + if ($association instanceof \Cake\ORM\Association && $association->type() === 'belongsToMany') { /** @var \Cake\ORM\Association\BelongsToMany $belongsToMany */ $belongsToMany = $association; $tables[] = $belongsToMany->junction()->getTable(); diff --git a/tests/TestCase/Command/ResetCommandTest.php b/tests/TestCase/Command/ResetCommandTest.php index 81a713ea1..f59081701 100644 --- a/tests/TestCase/Command/ResetCommandTest.php +++ b/tests/TestCase/Command/ResetCommandTest.php @@ -13,7 +13,7 @@ class ResetCommandTest extends TestCase { protected array $createdFiles = []; - public function setUp(): void + protected function setUp(): void { parent::setUp(); @@ -25,7 +25,7 @@ public function setUp(): void EventManager::instance()->off('Migration.afterReset'); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); foreach ($this->createdFiles as $file) { @@ -39,7 +39,7 @@ public function tearDown(): void protected function resetOutput(): void { - if ($this->_out) { + if ($this->_out instanceof \Cake\Console\TestSuite\StubConsoleOutput) { $property = new ReflectionProperty($this->_out, '_out'); $property->setValue($this->_out, []); } diff --git a/tests/TestCase/Db/Table/TableTest.php b/tests/TestCase/Db/Table/TableTest.php index 5d909d2ba..588c81595 100644 --- a/tests/TestCase/Db/Table/TableTest.php +++ b/tests/TestCase/Db/Table/TableTest.php @@ -90,7 +90,7 @@ public function testAddColumnWithNullTypeThrows(): void $table = new Table('ntable', [], $adapter); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Column type must not be null when column name is a string.'); - $table->addColumn('email', null); + $table->addColumn('email'); } public function testAddColumnWithNoAdapterSpecified(): void From 743a8937c09963505c444bbbe5da729e78144786 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 31 Mar 2026 04:23:48 +0200 Subject: [PATCH 28/72] Fix phpcs - use imports instead of FQN --- src/BaseSeed.php | 4 ++-- src/Db/Adapter/AbstractAdapter.php | 2 +- src/Util/TableFinder.php | 3 ++- tests/TestCase/Command/ResetCommandTest.php | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/BaseSeed.php b/src/BaseSeed.php index 1ea4ab9d1..8cde6fb73 100644 --- a/src/BaseSeed.php +++ b/src/BaseSeed.php @@ -263,8 +263,8 @@ protected function runCall(string $seeder, array $options = []): void $options += [ 'connection' => $connection, - 'plugin' => $pluginName ?? ($config instanceof \Migrations\Config\ConfigInterface ? $config['plugin'] : null), - 'source' => $config instanceof \Migrations\Config\ConfigInterface ? $config['source'] : null, + 'plugin' => $pluginName ?? ($config instanceof ConfigInterface ? $config['plugin'] : null), + 'source' => $config instanceof ConfigInterface ? $config['source'] : null, ]; $factory = new ManagerFactory([ 'connection' => $options['connection'], diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 903a43d68..052a74e30 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -194,7 +194,7 @@ public function getConnection(): Connection $this->connection = $this->getOption('connection'); $this->connect(); } - if (!$this->connection instanceof \Cake\Database\Connection) { + if (!$this->connection instanceof Connection) { throw new RuntimeException('Unable to establish database connection. Ensure a connection is configured.'); } diff --git a/src/Util/TableFinder.php b/src/Util/TableFinder.php index a894390aa..a15d49171 100644 --- a/src/Util/TableFinder.php +++ b/src/Util/TableFinder.php @@ -17,6 +17,7 @@ use Cake\Core\Plugin as CorePlugin; use Cake\Database\Schema\CollectionInterface; use Cake\Datasource\ConnectionManager; +use Cake\ORM\Association; use Cake\ORM\TableRegistry; use ReflectionClass; @@ -178,7 +179,7 @@ public function fetchTableName(string $className, ?string $pluginName = null): a $table = TableRegistry::getTableLocator()->get($className); foreach ($table->associations()->keys() as $key) { $association = $table->associations()->get($key); - if ($association instanceof \Cake\ORM\Association && $association->type() === 'belongsToMany') { + if ($association instanceof Association && $association->type() === 'belongsToMany') { /** @var \Cake\ORM\Association\BelongsToMany $belongsToMany */ $belongsToMany = $association; $tables[] = $belongsToMany->junction()->getTable(); diff --git a/tests/TestCase/Command/ResetCommandTest.php b/tests/TestCase/Command/ResetCommandTest.php index f59081701..7ccd45a72 100644 --- a/tests/TestCase/Command/ResetCommandTest.php +++ b/tests/TestCase/Command/ResetCommandTest.php @@ -3,6 +3,7 @@ namespace Migrations\Test\TestCase\Command; +use Cake\Console\TestSuite\StubConsoleOutput; use Cake\Datasource\ConnectionManager; use Cake\Event\EventInterface; use Cake\Event\EventManager; @@ -39,7 +40,7 @@ protected function tearDown(): void protected function resetOutput(): void { - if ($this->_out instanceof \Cake\Console\TestSuite\StubConsoleOutput) { + if ($this->_out instanceof StubConsoleOutput) { $property = new ReflectionProperty($this->_out, '_out'); $property->setValue($this->_out, []); } From b31ee2ec6018ae626ab1a0d43e90aac4f414ee18 Mon Sep 17 00:00:00 2001 From: Jamison Bryant Date: Mon, 30 Mar 2026 23:15:21 -0400 Subject: [PATCH 29/72] Add ALGORITHM/LOCK support to Index operations (#1057) * Add ALGORITHM/LOCK support to Index operations Extend the ALGORITHM and LOCK clause support added in PR #955 for Column operations to also cover Index operations. This allows addIndex to pass algorithm and lock options through the fluent API on MySQL. Changes: - Add algorithm/lock properties, getters/setters to Index class - Wire algorithm/lock through getAddIndexInstructions in MysqlAdapter - Handle FULLTEXT indexes which use post-steps (inline the clause) - Add 9 tests mirroring the Column algorithm/lock test coverage * Add tests for FULLTEXT index and batched lock conflict paths * Add SQL validation and fluent builder tests for ALGORITHM/LOCK Verify that ALGORITHM and LOCK clauses appear in generated SQL using verbose logging, and exercise the fluent Column/Index builder path for both column and index operations. --- src/Db/Adapter/MysqlAdapter.php | 21 ++ src/Db/Table/Index.php | 52 +++- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 276 ++++++++++++++++++ 3 files changed, 348 insertions(+), 1 deletion(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 3ff1883de..db13ed171 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -899,6 +899,20 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index): $this->getIndexSqlDefinition($index), ); + // FULLTEXT indexes use post-steps (raw SQL) which executeAlterSteps + // does not append algorithm/lock to, so we inline the clause here. + // Setting on instructions as well ensures validation still runs. + if ($index->getAlgorithm() !== null || $index->getLock() !== null) { + if ($index->getAlgorithm() !== null) { + $alter .= ', ALGORITHM=' . strtoupper($index->getAlgorithm()); + $instructions->setAlgorithm($index->getAlgorithm()); + } + if ($index->getLock() !== null) { + $alter .= ', LOCK=' . strtoupper($index->getLock()); + $instructions->setLock($index->getLock()); + } + } + $instructions->addPostStep($alter); } else { $alter = sprintf( @@ -907,6 +921,13 @@ protected function getAddIndexInstructions(TableMetadata $table, Index $index): ); $instructions->addAlter($alter); + + if ($index->getAlgorithm() !== null) { + $instructions->setAlgorithm($index->getAlgorithm()); + } + if ($index->getLock() !== null) { + $instructions->setLock($index->getLock()); + } } return $instructions; diff --git a/src/Db/Table/Index.php b/src/Db/Table/Index.php index f1ab782b4..af9653973 100644 --- a/src/Db/Table/Index.php +++ b/src/Db/Table/Index.php @@ -47,6 +47,8 @@ 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 ?string $algorithm The ALTER TABLE algorithm (MySQL-specific). + * @param ?string $lock The ALTER TABLE lock mode (MySQL-specific). */ public function __construct( protected string $name = '', @@ -57,6 +59,8 @@ public function __construct( protected ?array $include = null, protected ?string $where = null, protected bool $concurrent = false, + protected ?string $algorithm = null, + protected ?string $lock = null, ) { } @@ -149,6 +153,52 @@ public function getConcurrently(): bool return $this->concurrent; } + /** + * Sets the ALTER TABLE algorithm (MySQL-specific). + * + * @param string $algorithm Algorithm + * @return $this + */ + public function setAlgorithm(string $algorithm) + { + $this->algorithm = $algorithm; + + return $this; + } + + /** + * Gets the ALTER TABLE algorithm. + * + * @return string|null + */ + public function getAlgorithm(): ?string + { + return $this->algorithm; + } + + /** + * Sets the ALTER TABLE lock mode (MySQL-specific). + * + * @param string $lock Lock mode + * @return $this + */ + public function setLock(string $lock) + { + $this->lock = $lock; + + return $this; + } + + /** + * Gets the ALTER TABLE lock mode. + * + * @return string|null + */ + public function getLock(): ?string + { + return $this->lock; + } + /** * Utility method that maps an array of index options to this object's methods. * @@ -159,7 +209,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', 'algorithm', 'lock']; 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/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 17b1ef844..25649d501 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -3127,6 +3127,282 @@ public function testAlgorithmWithMixedCase(): void $this->assertTrue($this->adapter->hasColumn('mixed_case', 'col2')); } + public function testAddColumnWithAlgorithmAndLockSqlContainsClause(): void + { + $table = new Table('col_sql_verify', [], $this->adapter); + $table->addColumn('col1', 'string') + ->create(); + + $this->io->level(ConsoleIo::VERBOSE); + $this->out->clear(); + + $table->addColumn('col2', 'string', [ + 'null' => true, + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_NONE, + ])->update(); + + $output = $this->out->output(); + $this->assertStringContainsString('ALGORITHM=INPLACE', $output); + $this->assertStringContainsString('LOCK=NONE', $output); + $this->assertTrue($this->adapter->hasColumn('col_sql_verify', 'col2')); + } + + public function testAddColumnWithFluentColumnBuilder(): void + { + $table = new Table('col_fluent', [], $this->adapter); + $table->addColumn('col1', 'string') + ->create(); + + $column = new Column(); + $column->setName('col2') + ->setType('string') + ->setNull(true) + ->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE) + ->setLock(MysqlAdapter::LOCK_NONE); + + $this->io->level(ConsoleIo::VERBOSE); + $this->out->clear(); + + $table->addColumn($column)->update(); + + $output = $this->out->output(); + $this->assertStringContainsString('ALGORITHM=INPLACE', $output); + $this->assertStringContainsString('LOCK=NONE', $output); + $this->assertTrue($this->adapter->hasColumn('col_fluent', 'col2')); + } + + public function testAddIndexWithAlgorithm(): void + { + $table = new Table('index_algo', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + ])->update(); + + $this->assertTrue($this->adapter->hasIndex('index_algo', ['email'])); + } + + public function testAddIndexWithAlgorithmAndLock(): void + { + $table = new Table('index_algo_lock', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_NONE, + ])->update(); + + $this->assertTrue($this->adapter->hasIndex('index_algo_lock', ['email'])); + } + + public function testAddIndexWithAlgorithmCopy(): void + { + $table = new Table('index_copy', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_COPY, + ])->update(); + + $this->assertTrue($this->adapter->hasIndex('index_copy', ['email'])); + } + + public function testAddIndexWithAlgorithmMixedCase(): void + { + $table = new Table('index_case', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $table->addIndex('email', [ + 'algorithm' => 'inplace', + 'lock' => 'none', + ])->update(); + + $this->assertTrue($this->adapter->hasIndex('index_case', ['email'])); + } + + public function testAddIndexWithInvalidAlgorithmThrowsException(): void + { + $table = new Table('index_invalid_algo', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid algorithm'); + + $table->addIndex('email', [ + 'algorithm' => 'INVALID', + ])->update(); + } + + public function testAddIndexWithInvalidLockThrowsException(): void + { + $table = new Table('index_invalid_lock', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid lock'); + + $table->addIndex('email', [ + 'lock' => 'INVALID', + ])->update(); + } + + public function testAddIndexWithAlgorithmInstantAndExplicitLockThrowsException(): void + { + $table = new Table('index_instant_lock', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE'); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INSTANT, + 'lock' => MysqlAdapter::LOCK_NONE, + ])->update(); + } + + public function testBatchedIndexesWithSameAlgorithm(): void + { + $table = new Table('index_batch', [], $this->adapter); + $table->addColumn('email', 'string') + ->addColumn('name', 'string') + ->create(); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_NONE, + ]) + ->addIndex('name', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_NONE, + ]) + ->update(); + + $this->assertTrue($this->adapter->hasIndex('index_batch', ['email'])); + $this->assertTrue($this->adapter->hasIndex('index_batch', ['name'])); + } + + public function testBatchedIndexesWithConflictingAlgorithmsThrowsException(): void + { + $table = new Table('index_batch_conflict', [], $this->adapter); + $table->addColumn('email', 'string') + ->addColumn('name', 'string') + ->create(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Conflicting algorithm specifications'); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + ]) + ->addIndex('name', [ + 'algorithm' => MysqlAdapter::ALGORITHM_COPY, + ]) + ->update(); + } + + public function testBatchedIndexesWithConflictingLocksThrowsException(): void + { + $table = new Table('index_lock_conflict', [], $this->adapter); + $table->addColumn('email', 'string') + ->addColumn('name', 'string') + ->create(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Conflicting lock specifications'); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_NONE, + ]) + ->addIndex('name', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_SHARED, + ]) + ->update(); + } + + public function testAddFulltextIndexWithAlgorithmAndLock(): void + { + $table = new Table('index_fulltext_algo', [], $this->adapter); + $table->addColumn('content', 'text') + ->create(); + + $table->addIndex('content', [ + 'type' => 'fulltext', + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_SHARED, + ])->update(); + + $this->assertTrue($this->adapter->hasIndex('index_fulltext_algo', ['content'])); + } + + public function testAddFulltextIndexWithInstantAndLockThrowsException(): void + { + $table = new Table('index_fulltext_instant', [], $this->adapter); + $table->addColumn('content', 'text') + ->create(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('ALGORITHM=INSTANT cannot be combined with LOCK=NONE'); + + $table->addIndex('content', [ + 'type' => 'fulltext', + 'algorithm' => MysqlAdapter::ALGORITHM_INSTANT, + 'lock' => MysqlAdapter::LOCK_NONE, + ])->update(); + } + + public function testAddIndexWithAlgorithmAndLockSqlContainsClause(): void + { + $table = new Table('idx_sql_verify', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $this->io->level(ConsoleIo::VERBOSE); + $this->out->clear(); + + $table->addIndex('email', [ + 'algorithm' => MysqlAdapter::ALGORITHM_INPLACE, + 'lock' => MysqlAdapter::LOCK_NONE, + ])->update(); + + $output = $this->out->output(); + $this->assertStringContainsString('ALGORITHM=INPLACE', $output); + $this->assertStringContainsString('LOCK=NONE', $output); + $this->assertTrue($this->adapter->hasIndex('idx_sql_verify', ['email'])); + } + + public function testAddIndexWithFluentIndexBuilder(): void + { + $table = new Table('idx_fluent', [], $this->adapter); + $table->addColumn('email', 'string') + ->create(); + + $index = new Index(); + $index->setColumns('email') + ->setAlgorithm(MysqlAdapter::ALGORITHM_INPLACE) + ->setLock(MysqlAdapter::LOCK_NONE); + + $this->io->level(ConsoleIo::VERBOSE); + $this->out->clear(); + + $table->addIndex($index)->update(); + + $output = $this->out->output(); + $this->assertStringContainsString('ALGORITHM=INPLACE', $output); + $this->assertStringContainsString('LOCK=NONE', $output); + $this->assertTrue($this->adapter->hasIndex('idx_fluent', ['email'])); + } + public function testInsertOrUpdateWithDuplicates(): void { $table = new Table('currencies', [], $this->adapter); From 4120877d2501aee392a0bdeb65d5d6ece386f01e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:11:42 -0400 Subject: [PATCH 30/72] Bump codecov/codecov-action from 5 to 6 (#1063) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e551da118..a4ab32a37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: - name: Code Coverage Report if: success() && matrix.php-version == '8.1' && matrix.db-type == 'mysql' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 testsuite-windows: runs-on: windows-2022 @@ -240,7 +240,7 @@ jobs: vendor/bin/phpunit --coverage-clover=coverage.xml - name: Submit code coverage - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 cs-stan: uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x From 1e4c2d76b43c770770fd20393688b0ca12f9c1c8 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 4 Apr 2026 05:41:06 +0200 Subject: [PATCH 31/72] Fix documentation issues (#1064) - Update migrations documentation link: 3 -> 5 - Use strict comparison (===) instead of loose (==) --- docs/en/index.md | 2 +- docs/en/index.rst | 2 +- docs/en/seeding.md | 2 +- docs/en/seeding.rst | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/index.md b/docs/en/index.md index 6eb56c665..eabd7f7fd 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -188,7 +188,7 @@ class CreateProducts extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/3/en/writing-migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/en/writing-migrations.html#the-change-method * @return void */ public function change(): void diff --git a/docs/en/index.rst b/docs/en/index.rst index c0155fecd..f180413e9 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -197,7 +197,7 @@ The command line above will generate a migration file that resembles:: * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/3/en/writing-migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/en/writing-migrations.html#the-change-method * @return void */ public function change(): void diff --git a/docs/en/seeding.md b/docs/en/seeding.md index c2d21c501..8a78887f2 100644 --- a/docs/en/seeding.md +++ b/docs/en/seeding.md @@ -254,7 +254,7 @@ class ConfigSeed extends BaseSeed "SELECT COUNT(*) as count FROM settings WHERE setting_key = 'maintenance_mode'" ); - if ($exists['count'] == 0) { + if ($exists['count'] === 0) { $this->table('settings')->insert([ 'setting_key' => 'maintenance_mode', 'setting_value' => 'false', diff --git a/docs/en/seeding.rst b/docs/en/seeding.rst index 2d4838823..d2aedf34b 100644 --- a/docs/en/seeding.rst +++ b/docs/en/seeding.rst @@ -269,7 +269,7 @@ that update configuration or reference data. For these seeds, you can override t "SELECT COUNT(*) as count FROM settings WHERE setting_key = 'maintenance_mode'" ); - if ($exists['count'] == 0) { + if ($exists['count'] === 0) { $this->table('settings')->insert([ 'setting_key' => 'maintenance_mode', 'setting_value' => 'false', From 122ce9f856cd0316d9c4a1237bc7f6ca45d10caa Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sat, 4 Apr 2026 11:44:55 +0200 Subject: [PATCH 32/72] Add pluginname navbar --- .github/workflows/docs-validation.yml | 27 ++ docs/.vitepress/config.js | 2 + docs/package-lock.json | 578 ++++++++++++++------------ 3 files changed, 335 insertions(+), 272 deletions(-) create mode 100644 .github/workflows/docs-validation.yml diff --git a/.github/workflows/docs-validation.yml b/.github/workflows/docs-validation.yml new file mode 100644 index 000000000..439a52c18 --- /dev/null +++ b/.github/workflows/docs-validation.yml @@ -0,0 +1,27 @@ +name: Documentation Validation + +on: + push: + branches: + - 5.x + paths: + - 'docs/**' + - '.github/**' + pull_request: + paths: + - 'docs/**' + - '.github/**' + +jobs: + validate: + uses: cakephp/.github/.github/workflows/docs-validation.yml@5.x + with: + docs-path: 'docs' + vitepress-path: 'docs/.vitepress' + enable-config-js-check: true + enable-json-lint: true + enable-toc-check: true + enable-spell-check: true + enable-markdown-lint: true + enable-link-check: true + tools-ref: '5.x' diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 702f50b55..b32992f07 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -28,6 +28,8 @@ export default { hostname: "https://book.cakephp.org/migrations/5/", }, themeConfig: { + siteTitle: false, + pluginName: "Migrations", socialLinks: [ { icon: "github", link: "https://github.com/cakephp/cakephp" }, ], diff --git a/docs/package-lock.json b/docs/package-lock.json index f22cc50b3..68db1806d 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -61,7 +61,7 @@ }, "node_modules/@cakephp/docs-skeleton": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#beb4b50177ce8fd81d5113d2e3fc49b107ddf2f2", + "resolved": "git+ssh://git@github.com/cakephp/docs-skeleton.git#9501c6d47a80645604fceb31328368eeb6a21bc6", "bin": { "cakedocs": "bin/cakedocs.js" }, @@ -88,9 +88,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -136,9 +136,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -152,9 +152,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -168,9 +168,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -200,9 +200,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -216,9 +216,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -232,9 +232,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -248,9 +248,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -264,9 +264,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -280,9 +280,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -312,9 +312,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -328,9 +328,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -344,9 +344,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -360,9 +360,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -392,9 +392,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -408,9 +408,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -424,9 +424,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -440,9 +440,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -472,9 +472,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -488,9 +488,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -504,9 +504,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.75", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.75.tgz", - "integrity": "sha512-KvcCUbvcBWb0sbqLIxHoY8z5/piXY08wcY9gfMhF+ph3AfzGMaSmZFkUY71HSXAljQngXkgs4bdKdekO0HQWvg==", + "version": "1.2.76", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.76.tgz", + "integrity": "sha512-lLRlA8yaf+1L5VCPRvR9lynoSklsddKHEylchmZJKdj/q2xVQ1ZAEJ8SCQlv9cbgtMefnlyM98U+8Si2aoFZPA==", "license": "CC0-1.0", "dependencies": { "@iconify/types": "*" @@ -531,9 +531,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -544,9 +544,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -557,9 +557,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -570,9 +570,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -583,9 +583,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -596,9 +596,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -609,12 +609,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -622,12 +625,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -635,12 +641,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -648,12 +657,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -661,12 +673,15 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -674,12 +689,15 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -687,12 +705,15 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -700,12 +721,15 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -713,12 +737,15 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -726,12 +753,15 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -739,12 +769,15 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -752,12 +785,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -765,12 +801,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -778,9 +817,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], @@ -791,9 +830,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -804,9 +843,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -817,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -830,9 +869,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "cpu": [ "x64" ], @@ -843,9 +882,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -1013,39 +1052,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz", - "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.31", + "@vue/shared": "3.5.32", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz", - "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz", - "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.2", - "@vue/compiler-core": "3.5.31", - "@vue/compiler-dom": "3.5.31", - "@vue/compiler-ssr": "3.5.31", - "@vue/shared": "3.5.31", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", @@ -1053,13 +1092,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz", - "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/devtools-api": { @@ -1090,53 +1129,53 @@ "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz", - "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.31" + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz", - "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz", - "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.31", - "@vue/runtime-core": "3.5.31", - "@vue/shared": "3.5.31", + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz", - "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { - "vue": "3.5.31" + "vue": "3.5.32" } }, "node_modules/@vue/shared": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz", - "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", "license": "MIT" }, "node_modules/@vueuse/core": { @@ -1333,9 +1372,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1345,32 +1384,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/estree-walker": { @@ -1401,7 +1440,6 @@ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.0.1.tgz", "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==", "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -1655,7 +1693,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1726,9 +1763,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -1741,31 +1778,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, @@ -1951,7 +1988,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2026,7 +2062,6 @@ "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", "license": "MIT", - "peer": true, "dependencies": { "@docsearch/css": "^4.5.3", "@docsearch/js": "^4.5.3", @@ -2069,17 +2104,16 @@ } }, "node_modules/vue": { - "version": "3.5.31", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz", - "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==", + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", "license": "MIT", - "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.31", - "@vue/compiler-sfc": "3.5.31", - "@vue/runtime-dom": "3.5.31", - "@vue/server-renderer": "3.5.31", - "@vue/shared": "3.5.31" + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" }, "peerDependencies": { "typescript": "*" From 1f572a62f7685e4a828f1665f28e0ef5b4d1d89f Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Sat, 4 Apr 2026 11:55:33 +0200 Subject: [PATCH 33/72] Fix doc ci issues --- docs/en/executing-queries.md | 6 +- docs/en/index.md | 106 ++++++++--------- docs/en/seeding.md | 68 +++++------ docs/en/upgrading-to-builtin-backend.md | 22 ++-- docs/en/upgrading.md | 14 +-- docs/en/using-the-query-builder.md | 32 ++--- docs/en/writing-migrations.md | 148 ++++++++++++------------ docs/package.json | 2 +- 8 files changed, 199 insertions(+), 199 deletions(-) diff --git a/docs/en/executing-queries.md b/docs/en/executing-queries.md index a87c58dd0..3c6c3edd3 100644 --- a/docs/en/executing-queries.md +++ b/docs/en/executing-queries.md @@ -7,7 +7,7 @@ Queries can be executed with the `execute()` and `query()` methods. The accept an optional second parameter `$params` which is an array of elements, and if used will cause the underlying connection to use a prepared statement: -``` php +```php addPlugin('Migrations'); ``` @@ -67,7 +67,7 @@ A migration file can add, change or remove tables, columns, indexes and foreign If we wanted to create a table, we could use a migration similar to this: -``` php +```php run(['plugin' => 'Documents', 'connection' => 'test_docs']); If you need to run multiple sets of migrations, those can be run as follows: -``` php +```php // Run migrations for plugin Contacts on the ``test`` connection, and Documents on the ``test_docs`` connection $migrator->runMany([ ['plugin' => 'Contacts'], @@ -739,7 +739,7 @@ If your database also contains tables that are not managed by your application like those created by PostGIS, then you can exclude those tables from the drop & truncate behavior using the `skip` option: -``` php +```php $migrator->run(['connection' => 'test', 'skip' => ['postgis*']]); ``` @@ -756,7 +756,7 @@ to be distributed much more portable and easy to install. All commands in the Migrations plugin support the `--plugin` or `-p` option that will scope the execution to the migrations relative to that plugin: -``` bash +```bash bin/cake migrations status -p PluginName bin/cake migrations migrate -p PluginName @@ -780,7 +780,7 @@ Each of these commands has a method defined in the `Migrations` class. Here is how to use it: -``` php +```php use Migrations\Migrations; $migrations = new Migrations(); @@ -804,7 +804,7 @@ $seeded = $migrations->seed(); The methods can accept an array of parameters that should match options from the commands: -``` php +```php use Migrations\Migrations; $migrations = new Migrations(); @@ -822,7 +822,7 @@ Optionally, you can pass these parameters in the constructor of the class. They will be used as default and this will prevent you from having to pass them on each method call: -``` php +```php use Migrations\Migrations; $migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); @@ -835,7 +835,7 @@ $migrate = $migrations->migrate(); If you need to override one or more default parameters for one call, you can pass them to the method: -``` php +```php use Migrations\Migrations; $migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); @@ -860,7 +860,7 @@ Migrations offers a few feature flags for compatibility. These features are disa Set them via Configure to enable (e.g. in `config/app.php`): -``` text +```text 'Migrations' => [ 'unsigned_primary_keys' => true, 'unsigned_ints' => true, @@ -883,7 +883,7 @@ database schema at any given point in time. You can skip this file generation, for instance when deploying on your production environment, by using the `--no-lock` option for the aforementioned command: -``` bash +```bash bin/cake migrations migrate --no-lock bin/cake migrations rollback --no-lock @@ -903,7 +903,7 @@ columns not existing when performing operations on those new columns. The CakePHP Core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you can use to perform this operation: -``` bash +```bash bin/cake migration migrate bin/cake schema_cache clear ``` @@ -913,7 +913,7 @@ bin/cake schema_cache clear You can use the `Migrations.PendingMigrations` middleware in local development to alert developers about new migrations that have not been applied: -``` php +```php use Migrations\Middleware\PendingMigrationsMiddleware; $config = [ diff --git a/docs/en/seeding.md b/docs/en/seeding.md index 8a78887f2..f1914aad1 100644 --- a/docs/en/seeding.md +++ b/docs/en/seeding.md @@ -11,13 +11,13 @@ it's created. By default, they are stored in the `config/Seeds` directory. Migrations includes a command to easily generate a new seed class: -``` bash -$ bin/cake bake seed MyNewSeed +```bash +bin/cake bake seed MyNewSeed ``` By default, it generates a traditional seed class with a named class: -``` php +```php [ 'style' => 'anonymous', // or 'traditional' @@ -111,7 +111,7 @@ You can set the default seed style globally in your application configuration: ### Seed Options -``` bash +```bash # you specify the name of the table the seed files will alter by using the ``--table`` option bin/cake bake seed articles --table my_articles_table # You can specify a plugin to bake into @@ -127,7 +127,7 @@ bin/cake bake seed --data Articles By default, it will export all the rows found in your table. You can limit the number of rows exported by using the `--limit` option: -``` bash +```bash # Will only export the first 10 rows found bin/cake bake seed --data --limit 10 Articles ``` @@ -136,7 +136,7 @@ If you only want to include a selection of fields from the table in your seed file, you can use the `--fields` option. It takes the list of fields to include as a comma separated value string: -``` bash +```bash # Will only export the fields `id`, `title` and `excerpt` bin/cake bake seed --data --fields id,title,excerpt Articles ``` @@ -182,19 +182,19 @@ executed, it will be skipped with an "already executed" message. To re-run a seed that has already been executed, use the `--force` flag: -``` bash +```bash bin/cake seeds run Users --force ``` You can check which seeds have been executed using the status command: -``` bash +```bash bin/cake seeds status ``` To reset all seeds' execution state (allowing them to run again without `--force`): -``` bash +```bash bin/cake seeds reset ``` @@ -208,7 +208,7 @@ By default, seed execution is tracked in a table named `cake_seeds`. You can customize this table name by configuring it in your `config/app.php` or `config/app_local.php`: -``` php +```php 'Migrations' => [ 'seed_table' => 'my_custom_seeds_table', ], @@ -223,7 +223,7 @@ Some seeds are designed to be run multiple times safely (idempotent), such as se that update configuration or reference data. For these seeds, you can override the `isIdempotent()` method: -``` php +```php call('AnotherSeed'); $this->call('YetAnotherSeed'); $this->call('PluginName.FromPluginSeed'); @@ -397,7 +397,7 @@ Seed classes can also use the familiar `Table` object to insert data. You can retrieve an instance of the Table object by calling the `table()` method from within your seed class and then use the `insert()` method to insert data: -``` php +```php TRUNCATE command: -``` php +```php -v parameter for more output verbosity: -``` bash -$ bin/cake seeds run -v +```bash +bin/cake seeds run -v ``` The Migrations seed functionality provides a simple mechanism to easily and repeatably diff --git a/docs/en/upgrading-to-builtin-backend.md b/docs/en/upgrading-to-builtin-backend.md index 59a034c1b..4668273a4 100644 --- a/docs/en/upgrading-to-builtin-backend.md +++ b/docs/en/upgrading-to-builtin-backend.md @@ -22,7 +22,7 @@ commands have been removed and replaced with new command names: **Seeds:** -``` bash +```bash # Old (4.x and earlier) bin/cake migrations seed bin/cake migrations seed --seed Articles @@ -45,7 +45,7 @@ If you need to maintain the old command structure for existing scripts or CI/CD pipelines, you can add command aliases in your application. In your `src/Application.php` file, add the following to the `console()` method: -``` php +```php public function console(CommandCollection $commands): CommandCollection { // Add your application's commands @@ -60,7 +60,7 @@ public function console(CommandCollection $commands): CommandCollection For multiple aliases, you can add them all together: -``` php +```php // Add multiple backward compatibility aliases $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); $commands->add('migrations seed:run', \Migrations\Command\SeedCommand::class); @@ -78,7 +78,7 @@ execute queries, the return of this method is now `Cake\Database\StatementInterface` instead. This impacts `fetchAll()`, and `fetch()`: -``` php +```php // This $stmt = $this->getAdapter()->query('SELECT * FROM articles'); $rows = $stmt->fetchAll(); @@ -90,7 +90,7 @@ $rows = $stmt->fetchAll('assoc'); Similar changes are for fetching a single row: -``` php +```php // This $stmt = $this->getAdapter()->query('SELECT * FROM articles'); $rows = $stmt->fetch(); @@ -124,7 +124,7 @@ For existing applications with `phinxlog` tables: The `Migrations.legacyTables` configuration option controls the behavior: -``` php +```php // config/app.php or config/app_local.php 'Migrations' => [ // null (default): Autodetect - use legacy if phinxlog tables exist @@ -138,19 +138,19 @@ The `Migrations.legacyTables` configuration option controls the behavior: To migrate from `phinxlog` tables to the new `cake_migrations` table: -1. **Preview the upgrade** (dry run): +1. **Preview the upgrade** (dry run): ``` bash bin/cake migrations upgrade --dry-run ``` -2. **Run the upgrade**: +2. **Run the upgrade**: ``` bash bin/cake migrations upgrade ``` -3. **Update your configuration**: +3. **Update your configuration**: ``` php // config/app.php @@ -159,7 +159,7 @@ To migrate from `phinxlog` tables to the new `cake_migrations` table: ], ``` -4. **Optionally drop phinx tables**: Your migration history is preserved +4. **Optionally drop phinx tables**: Your migration history is preserved by default. Use `--drop-tables` to drop the `phinxlog`tables after verifying your migrations run correctly. @@ -171,7 +171,7 @@ To migrate from `phinxlog` tables to the new `cake_migrations` table: If you need to revert to phinx tables after upgrading: -1. Set `'legacyTables' => true` in your configuration. +1. Set `'legacyTables' => true` in your configuration. > [!WARNING] > You cannot rollback after running `upgrade --drop-tables`. diff --git a/docs/en/upgrading.md b/docs/en/upgrading.md index 75b95a539..c34d6c36a 100644 --- a/docs/en/upgrading.md +++ b/docs/en/upgrading.md @@ -21,7 +21,7 @@ The phinx wrapper commands have been removed. The new command structure is: The migration commands remain unchanged: -``` bash +```bash bin/cake migrations migrate bin/cake migrations rollback bin/cake migrations status @@ -33,7 +33,7 @@ bin/cake migrations dump Seed commands have changed: -``` bash +```bash # 4.x # 5.x bin/cake migrations seed bin/cake seeds run bin/cake migrations seed --seed X bin/cake seeds run X @@ -51,7 +51,7 @@ The new seed commands are: If you need to maintain the old `migrations seed` command for existing scripts or CI/CD pipelines, you can add command aliases in your `src/Application.php`: -``` php +```php public function console(CommandCollection $commands): CommandCollection { $commands = $this->addConsoleCommands($commands); @@ -83,7 +83,7 @@ If you have code that directly references any of these classes, you will need to If your migrations use `AdapterInterface::query()` to fetch rows, the return type has changed from a phinx result to `Cake\Database\StatementInterface`: -``` php +```php // 4.x (phinx) $stmt = $this->getAdapter()->query('SELECT * FROM articles'); $rows = $stmt->fetchAll(); @@ -138,10 +138,10 @@ the generated names may differ between old and new migrations. This could cause **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 +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 +3. The auto-generated names include conflict resolution - if `{table}_{columns}` already exists, a counter suffix is added (`_2`, `_3`, etc.) **Name length limits:** diff --git a/docs/en/using-the-query-builder.md b/docs/en/using-the-query-builder.md index 297d11359..931c2d7dd 100644 --- a/docs/en/using-the-query-builder.md +++ b/docs/en/using-the-query-builder.md @@ -9,7 +9,7 @@ The Query builder is provided by the [cakephp/database](https://github.com/cakep be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the `getQueryBuilder(string $type)` function. The `string $type` options are 'select', 'insert', 'update' and \`'delete'\`: -``` php +```php select(['id', 'title', 'body']); @@ -49,7 +49,7 @@ $builder->select(function ($builder) { Generating conditions: -``` php +```php // WHERE id = 1 $builder->where(['id' => 1]); @@ -59,7 +59,7 @@ $builder->where(['id >' => 1]); As you can see you can use any operator by placing it with a space after the field name. Adding multiple conditions is easy as well: -``` php +```php where(['id >' => 1])->andWhere(['title' => 'My Title']); @@ -72,7 +72,7 @@ $builder->where(['OR' => ['id >' => 1, 'title' => 'My title']]); For even more complex conditions you can use closures and expression objects: -``` php +```php select('*') @@ -116,7 +116,7 @@ $builder It generates: -``` sql +```sql SELECT * FROM articles WHERE @@ -141,7 +141,7 @@ When using the expression objects you can use the following methods to create co ## Aggregates and SQL Functions -``` php +```php select(['count' => $builder->func()->count('*')]); @@ -163,7 +163,7 @@ When providing arguments for SQL functions, there are two kinds of parameters yo literal arguments and bound parameters. Literal parameters allow you to reference columns or other SQL literals. Bound parameters can be used to safely add user data to SQL functions. For example: -``` php +```php select(['title' => $concat]); Once you’ve made your query, you’ll want to retrieve rows from it. There are a few ways of doing this: -``` php +```php execute()->fetchAll('assoc'); Creating insert queries is also possible: -``` php +```php getQueryBuilder('insert'); $builder @@ -206,7 +206,7 @@ $builder For increased performance, you can use another builder object as the values for an insert query: -``` php +```php getQueryBuilder('select'); @@ -227,7 +227,7 @@ var_dump($st->lastInsertId('names', 'id')); The above code will generate: -``` sql +```sql INSERT INTO names (first_name, last_name) (SELECT fname, lname FROM USERS where is_active = 1) ``` @@ -236,7 +236,7 @@ INSERT INTO names (first_name, last_name) Creating update queries is similar to both inserting and selecting: -``` php +```php getQueryBuilder('update'); $builder @@ -250,7 +250,7 @@ $builder Finally, delete queries: -``` php +```php getQueryBuilder('delete'); $builder diff --git a/docs/en/writing-migrations.md b/docs/en/writing-migrations.md index 162515f1b..fd2fd88bb 100644 --- a/docs/en/writing-migrations.md +++ b/docs/en/writing-migrations.md @@ -8,8 +8,8 @@ your migrations using the Migrations API, but raw SQL is also supported. Let's start by creating a new migration with `bake`: -``` bash -$ bin/cake bake migration +```bash +bin/cake bake migration ``` This will create a new migration in the format @@ -21,7 +21,7 @@ which path to create the new migration in. Bake will automatically creates a skeleton migration file with a single method: -``` php +```php [ 'style' => 'anonymous', // or 'traditional' @@ -100,7 +100,7 @@ Migrations supports 'reversible migrations'. In many scenarios, you only need to define the `up` logic, and Migrations can figure out how to generate the rollback operations for you. For example: -``` php +```php isMigratingUp()` to only run things in the up or down direction. For example: -``` php +```php updateColumn('email', null, ['null' => true]); @@ -1077,9 +1077,9 @@ The following attributes are automatically preserved by `updateColumn()`: To completely replace a column definition, use the `changeColumn()` method. This method requires you to specify all desired column attributes. -See [Valid Column Types](#valid-column-types) and [Valid Column Options](#valid-column-options) for allowed values: +See [Adding Columns](#adding-columns) and [Valid Column Options](#valid-column-options) for allowed values: -``` php +```php true` in the options: -``` php +```php $table->changeColumn('email', 'string', [ 'null' => true, 'preserveUnspecified' => true, @@ -1131,7 +1131,7 @@ $table->changeColumn('email', 'string', [ To add an index to a table you can simply call the `addIndex()` method on the table object: -``` php +```php true]`. Foreign keys can be defined with arrays of columns to build constraints between tables with composite keys: -``` php +```php Date: Sat, 4 Apr 2026 12:01:21 +0200 Subject: [PATCH 34/72] docs(ci): fix remaining code fence issues --- docs/en/upgrading-to-builtin-backend.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/upgrading-to-builtin-backend.md b/docs/en/upgrading-to-builtin-backend.md index 4668273a4..f6a6f2a19 100644 --- a/docs/en/upgrading-to-builtin-backend.md +++ b/docs/en/upgrading-to-builtin-backend.md @@ -140,19 +140,19 @@ To migrate from `phinxlog` tables to the new `cake_migrations` table: 1. **Preview the upgrade** (dry run): - ``` bash + ```bash bin/cake migrations upgrade --dry-run ``` 2. **Run the upgrade**: - ``` bash + ```bash bin/cake migrations upgrade ``` 3. **Update your configuration**: - ``` php + ```php // config/app.php 'Migrations' => [ 'legacyTables' => false, @@ -163,7 +163,7 @@ To migrate from `phinxlog` tables to the new `cake_migrations` table: by default. Use `--drop-tables` to drop the `phinxlog`tables after verifying your migrations run correctly. - ``` bash + ```bash bin/cake migrations upgrade --drop-tables ``` From 5a796acfb21eaaa210b6c3bc2c3bff297420a0b3 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sat, 4 Apr 2026 20:35:57 +0200 Subject: [PATCH 35/72] adjust Dockerfile to use NodeJS 24 and angie --- Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 832f6bc1c..d1f129524 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ---------------------- # 1. Build stage # ---------------------- -FROM node:22-alpine AS builder +FROM node:24-alpine AS builder # Git is required because docs/package.json pulls a dependency from GitHub. RUN apk add --no-cache git openssh-client @@ -13,18 +13,18 @@ COPY docs/ ./ RUN npm ci # Increase max-old-space-size to avoid memory issues during build -ENV NODE_OPTIONS="--max-old-space-size=8192" +#ENV NODE_OPTIONS="--max-old-space-size=8192" # Build the site. RUN npm run docs:build # ---------------------- -# 2. Runtime stage (nginx) +# 2. Runtime stage (angie) # ---------------------- -FROM nginx:1.27-alpine AS runner +FROM docker.angie.software/angie:latest AS runner # Copy built files -COPY --from=builder /app/docs/.vitepress/dist /usr/share/nginx/html +COPY --from=builder /app/docs/.vitepress/dist /usr/share/angie/html # Expose port EXPOSE 80 @@ -32,5 +32,5 @@ EXPOSE 80 # Health check (optional) HEALTHCHECK CMD wget --quiet --tries=1 --spider http://localhost:80/ || exit 1 -# Start nginx -CMD ["nginx", "-g", "daemon off;"] +# Start angie +CMD ["angie", "-g", "daemon off;"] From 121bcd14fec5aec0eecf204691a9df10d0036fb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:54:17 +0200 Subject: [PATCH 36/72] Bump vite from 7.3.1 to 7.3.2 in /docs (#1065) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 45 +++--------------------------------------- 1 file changed, 3 insertions(+), 42 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 68db1806d..3c6ad61c8 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -615,9 +615,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -631,9 +628,6 @@ "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -647,9 +641,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -663,9 +654,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -679,9 +667,6 @@ "cpu": [ "loong64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -695,9 +680,6 @@ "cpu": [ "loong64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -711,9 +693,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -727,9 +706,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -743,9 +719,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -759,9 +732,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -775,9 +745,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -791,9 +758,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -807,9 +771,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1984,9 +1945,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", From 22ddfff2c234b345f43ee13f1630f3583ad910d2 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sat, 11 Apr 2026 20:18:26 +0200 Subject: [PATCH 37/72] fix property types related to CakePHP 5.4 changes --- src/Command/BakeSeedCommand.php | 4 ++-- src/Command/BakeSimpleMigrationCommand.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index cecf2ec99..4129df76f 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -40,7 +40,7 @@ class BakeSeedCommand extends SimpleBakeCommand /** * Arguments */ - protected ?Arguments $args = null; + protected Arguments $args; /** * @inheritDoc @@ -84,7 +84,7 @@ public function getPath(Arguments $args): string */ public function template(): string { - $style = $this->args?->getOption('style') ?? Configure::read('Migrations.style', 'traditional'); + $style = $this->args->getOption('style') ?? Configure::read('Migrations.style', 'traditional'); if ($style === 'anonymous') { return 'Migrations.Seed/seed-anonymous'; } diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php index 622f4d1cb..d99b63a76 100644 --- a/src/Command/BakeSimpleMigrationCommand.php +++ b/src/Command/BakeSimpleMigrationCommand.php @@ -50,12 +50,12 @@ abstract class BakeSimpleMigrationCommand extends SimpleBakeCommand /** * Console IO */ - protected ?ConsoleIo $io = null; + protected ConsoleIo $io; /** * Arguments */ - protected ?Arguments $args = null; + protected Arguments $args; /** * @inheritDoc From 7e46d612e11ab88d954a46a686bf226c2ec30bf0 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 17 Apr 2026 05:30:58 +0200 Subject: [PATCH 38/72] Add support for PostgreSQL index access methods (gist, spgist, brin, hash) (#1053) * 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 * Fix CI failures: add trailing commas and fix tests for missing pg_trgm extension * Fix GiST test to use int4range type and add Exception import * Fix docs to use VitePress markdown instead of legacy rst Move PostgreSQL index access method documentation from the legacy writing-migrations.rst file into writing-migrations.md, which is the active VitePress source. Also document the snapshot reflection caveat until CakePHP 5.4 ships the reflection side. --- docs/en/writing-migrations.md | 116 ++++++++++++- src/Db/Adapter/PostgresAdapter.php | 38 ++++- src/Db/Table/Index.php | 72 +++++++- .../Db/Adapter/PostgresAdapterTest.php | 157 ++++++++++++++++++ 4 files changed, 369 insertions(+), 14 deletions(-) diff --git a/docs/en/writing-migrations.md b/docs/en/writing-migrations.md index fd2fd88bb..b415820e0 100644 --- a/docs/en/writing-migrations.md +++ b/docs/en/writing-migrations.md @@ -1335,7 +1335,14 @@ class MyNewMigration extends BaseMigration } ``` -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: ```php 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: + +```php +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: + +```php +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: + +```php +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: + +```php +table('sessions'); + $table->addColumn('session_id', 'string', ['limit' => 64]) + ->addIndex('session_id', ['type' => 'hash']) + ->create(); + } +} +``` + +::: warning +Until CakePHP 5.4 ships the reflection side of these access methods, schema +snapshots generated from tables using non-default index types may be lossy - +the `USING ` clause and `opclass` options are not yet reflected back +into generated migration code. +::: + Removing indexes is as easy as calling the `removeIndex()` method. You must call this method for each index: diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 7fe0f7446..3ff57a1fe 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -58,7 +58,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 @@ -925,8 +936,16 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin } $order = $index->getOrder() ?? []; - $columnNames = array_map(function (string $columnName) use ($order): string { + $opclass = $index->getOpclass() ?? []; + $columnNames = array_map(function (string $columnName) use ($order, $opclass): string { $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]; } @@ -937,11 +956,11 @@ 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 = ''; $whereClause = $index->getWhere(); @@ -950,11 +969,12 @@ protected function getIndexSqlDefinition(Index $index, string $tableName): strin } 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($indexName), $this->quoteTableName($tableName), + $usingClause, implode(',', $columnNames), $includedColumns, $where, diff --git a/src/Db/Table/Index.php b/src/Db/Table/Index.php index af9653973..507509af9 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 * @@ -49,6 +89,7 @@ class Index extends DatabaseIndex * @param bool $concurrent Whether to create the index concurrently. * @param ?string $algorithm The ALTER TABLE algorithm (MySQL-specific). * @param ?string $lock The ALTER TABLE lock mode (MySQL-specific). + * @param array|null $opclass The operator class for each column (PostgreSQL). */ public function __construct( protected string $name = '', @@ -61,6 +102,7 @@ public function __construct( protected bool $concurrent = false, protected ?string $algorithm = null, protected ?string $lock = null, + protected ?array $opclass = null, ) { } @@ -199,6 +241,34 @@ public function getLock(): ?string return $this->lock; } + /** + * 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. * @@ -209,7 +279,7 @@ public function getLock(): ?string public function setOptions(array $options) { // Valid Options - $validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'algorithm', 'lock']; + $validOptions = ['concurrently', 'type', 'unique', 'name', 'limit', 'order', 'include', 'where', 'algorithm', 'lock', '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 22ce3bf91..8e9fb2530 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; @@ -1497,6 +1498,162 @@ public function testDropIndexByNameWithSchema(): void $this->adapter->dropSchema('schema1'); } + public function testAddGistIndex(): void + { + // 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 = new Table('table1', [], $this->adapter); + $table->addIndex('int_range', ['type' => 'gist']) + ->save(); + + $this->assertTrue($table->hasIndex('int_range')); + + // 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_int_range'", + ); + $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 + { + // SP-GiST indexes on text require the text_ops operator class + $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 + { + // 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) { + $this->markTestSkipped('pg_trgm extension is not available'); + } + + $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(): void { $refTable = new Table('ref_table', [], $this->adapter); From 73b4bb9c91bdc21152ca279a186c4dd000c42a55 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Fri, 17 Apr 2026 13:37:46 +0200 Subject: [PATCH 39/72] rework docs to have more smaller pages --- docs/.vitepress/toc_en.json | 36 +- .../en/advanced/integration-and-deployment.md | 229 ++ docs/en/contents.md | 8 - .../en/getting-started/creating-migrations.md | 314 ++ .../installation-and-overview.md | 101 + .../running-and-managing-migrations.md | 135 + .../en/getting-started/snapshots-and-diffs.md | 116 + docs/en/{ => guides}/executing-queries.md | 57 - docs/en/guides/inserting-data.md | 56 + docs/en/{ => guides}/seeding.md | 0 .../{ => guides}/using-the-query-builder.md | 0 .../columns-and-table-operations.md | 388 +++ docs/en/guides/writing-migrations/index.md | 21 + .../indexes-and-constraints.md | 148 + .../writing-migrations/migration-methods.md | 135 + ...-introspection-and-platform-limitations.md | 105 + docs/en/index.md | 859 +----- docs/en/index.rst | 955 ------- docs/en/seeding.rst | 616 ---- .../upgrading-from-4-x.md} | 4 +- .../upgrading-to-builtin-backend.md | 0 docs/en/upgrading.rst | 174 -- docs/en/writing-migrations.md | 2527 ----------------- docs/en/writing-migrations.rst | 0 24 files changed, 1846 insertions(+), 5138 deletions(-) create mode 100644 docs/en/advanced/integration-and-deployment.md delete mode 100644 docs/en/contents.md create mode 100644 docs/en/getting-started/creating-migrations.md create mode 100644 docs/en/getting-started/installation-and-overview.md create mode 100644 docs/en/getting-started/running-and-managing-migrations.md create mode 100644 docs/en/getting-started/snapshots-and-diffs.md rename docs/en/{ => guides}/executing-queries.md (60%) create mode 100644 docs/en/guides/inserting-data.md rename docs/en/{ => guides}/seeding.md (100%) rename docs/en/{ => guides}/using-the-query-builder.md (100%) create mode 100644 docs/en/guides/writing-migrations/columns-and-table-operations.md create mode 100644 docs/en/guides/writing-migrations/index.md create mode 100644 docs/en/guides/writing-migrations/indexes-and-constraints.md create mode 100644 docs/en/guides/writing-migrations/migration-methods.md create mode 100644 docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md delete mode 100644 docs/en/index.rst delete mode 100644 docs/en/seeding.rst rename docs/en/{upgrading.md => upgrades/upgrading-from-4-x.md} (97%) rename docs/en/{ => upgrades}/upgrading-to-builtin-backend.md (100%) delete mode 100644 docs/en/upgrading.rst delete mode 100644 docs/en/writing-migrations.md delete mode 100644 docs/en/writing-migrations.rst diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json index 788a3039e..a50f01a4e 100644 --- a/docs/.vitepress/toc_en.json +++ b/docs/.vitepress/toc_en.json @@ -1,21 +1,43 @@ { "/": [ { - "text": "Migrations", + "text": "Getting Started", "collapsed": false, "items": [ - { "text": "Writing Migrations", "link": "/writing-migrations" }, - { "text": "Using the Query Builder", "link": "/using-the-query-builder" }, - { "text": "Executing Queries", "link": "/executing-queries" }, - { "text": "Database Seeding", "link": "/seeding" } + { "text": "Migrations 5.x", "link": "/" }, + { "text": "Installation and Overview", "link": "/getting-started/installation-and-overview" }, + { "text": "Creating Migrations", "link": "/getting-started/creating-migrations" }, + { "text": "Snapshots and Diffs", "link": "/getting-started/snapshots-and-diffs" }, + { "text": "Running and Managing Migrations", "link": "/getting-started/running-and-managing-migrations" } + ] + }, + { + "text": "Guides", + "collapsed": false, + "items": [ + { "text": "Integration and Deployment", "link": "/advanced/integration-and-deployment" }, + { + "text": "Writing Migrations", + "link": "/guides/writing-migrations/migration-methods", + "items": [ + { "text": "Migration Methods", "link": "/guides/writing-migrations/migration-methods" }, + { "text": "Columns and Table Operations", "link": "/guides/writing-migrations/columns-and-table-operations" }, + { "text": "Indexes and Constraints", "link": "/guides/writing-migrations/indexes-and-constraints" }, + { "text": "Schema Introspection and Platform Limitations", "link": "/guides/writing-migrations/schema-introspection-and-platform-limitations" } + ] + }, + { "text": "Using the Query Builder", "link": "/guides/using-the-query-builder" }, + { "text": "Executing Queries", "link": "/guides/executing-queries" }, + { "text": "Inserting Data", "link": "/guides/inserting-data" }, + { "text": "Database Seeding", "link": "/guides/seeding" } ] }, { "text": "Upgrade Guides", "collapsed": true, "items": [ - { "text": "Upgrading from 4.x to 5.x", "link": "/upgrading" }, - { "text": "Upgrading to the Builtin Backend", "link": "/upgrading-to-builtin-backend" } + { "text": "Upgrading from 4.x to 5.x", "link": "/upgrades/upgrading-from-4-x" }, + { "text": "Upgrading to the Builtin Backend", "link": "/upgrades/upgrading-to-builtin-backend" } ] } ] diff --git a/docs/en/advanced/integration-and-deployment.md b/docs/en/advanced/integration-and-deployment.md new file mode 100644 index 000000000..35be96e63 --- /dev/null +++ b/docs/en/advanced/integration-and-deployment.md @@ -0,0 +1,229 @@ +# Integration and Deployment + +This guide covers test integration, plugin usage, programmatic execution, and +deployment-related operational concerns. + +## Using Migrations for Tests + +If you are using migrations for your application schema, you can also use those +same migrations to build schema in your tests. In your application's +`tests/bootstrap.php` file you can use the `Migrator` class to build schema +when tests are run. The `Migrator` will use existing schema if it is current, +and if the migration history in the database differs from what is in the +filesystem, all tables will be dropped and migrations will be rerun from the +beginning: + +```php +// in tests/bootstrap.php +use Migrations\TestSuite\Migrator; + +$migrator = new Migrator(); + +// Simple setup with no plugins +$migrator->run(); + +// Run a non-test database +$migrator->run(['connection' => 'test_other']); + +// Run migrations for plugins +$migrator->run(['plugin' => 'Contacts']); + +// Run the Documents migrations on the test_docs connection +$migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']); +``` + +If you need to run multiple sets of migrations, use `runMany()`: + +```php +$migrator->runMany([ + ['plugin' => 'Contacts'], + ['plugin' => 'Documents', 'connection' => 'test_docs'], +]); +``` + +If your database also contains tables that are not managed by your application, +such as those created by PostGIS, you can exclude those tables from drop and +truncate behavior using the `skip` option: + +```php +$migrator->run(['connection' => 'test', 'skip' => ['postgis*']]); +``` + +The `skip` option accepts an `fnmatch()` compatible pattern. + +## Using Migrations in Plugins + +Plugins can also provide migration files. All commands in the Migrations plugin +support the `--plugin` or `-p` option to scope execution to the migrations +relative to that plugin: + +```bash +bin/cake migrations status -p PluginName +bin/cake migrations migrate -p PluginName +``` + +## Running Migrations in a Non-shell Environment + +While typical usage of migrations is from the command line, you can also run +migrations from a non-shell environment by using the `Migrations\Migrations` +class. This can be handy when you are developing a plugin installer for a CMS, +for instance. + +The `Migrations` class allows you to run the following commands: + +- `migrate` +- `rollback` +- `markMigrated` +- `status` +- `seed` + +Each of these commands has a corresponding method defined in the +`Migrations\Migrations` class. + +```php +use Migrations\Migrations; + +$migrations = new Migrations(); + +$status = $migrations->status(); +$migrate = $migrations->migrate(); +$rollback = $migrations->rollback(); +$markMigrated = $migrations->markMigrated(20150804222900); +$seeded = $migrations->seed(); +``` + +The methods can accept an array of parameters that match the shell command +options: + +```php +use Migrations\Migrations; + +$migrations = new Migrations(); +$status = $migrations->status([ + 'connection' => 'custom', + 'source' => 'MyMigrationsFolder', +]); +``` + +The only exception is `markMigrated()`, which expects the version number as the +first argument and the options array as the second. + +Optionally, you can pass default parameters in the constructor: + +```php +use Migrations\Migrations; + +$migrations = new Migrations([ + 'connection' => 'custom', + 'source' => 'MyMigrationsFolder', +]); + +$status = $migrations->status(); +$migrate = $migrations->migrate(); +``` + +If you need to override one or more default parameters for one call, pass them +to the method: + +```php +use Migrations\Migrations; + +$migrations = new Migrations([ + 'connection' => 'custom', + 'source' => 'MyMigrationsFolder', +]); + +$status = $migrations->status(); +$migrate = $migrations->migrate(['connection' => 'default']); +``` + +## Feature Flags + +Migrations offers a few feature flags for compatibility. These features are +disabled by default but can be enabled if required: + +- `unsigned_primary_keys`: Should Migrations create primary keys as unsigned + integers? Default: `false` +- `unsigned_ints`: Should Migrations create all integer columns as unsigned? + Default: `false` +- `column_null_default`: Should Migrations create columns as nullable by + default? Default: `false` +- `add_timestamps_use_datetime`: Should Migrations use `DATETIME` type columns + for the columns added by `addTimestamps()`? + +Set them via `Configure`, for example in `config/app.php`: + +```text +'Migrations' => [ + 'unsigned_primary_keys' => true, + 'unsigned_ints' => true, + 'column_null_default' => true, +], +``` + +> [!NOTE] +> The `unsigned_primary_keys` and `unsigned_ints` options only affect MySQL +> databases. When generating migrations with `bake migration_snapshot` or +> `bake migration_diff`, the `signed` attribute will only be included in the +> output for unsigned columns as `'signed' => false`. + +## Skipping the `schema.lock` File Generation + +In order for the diff feature to work, a `.lock` file is generated every time +you migrate, roll back, or bake a snapshot, to keep track of the state of your +database schema at any given point in time. You can skip this file generation, +for instance when deploying to production, by using the `--no-lock` option: + +```bash +bin/cake migrations migrate --no-lock +bin/cake migrations rollback --no-lock +bin/cake bake migration_snapshot MyMigration --no-lock +``` + +## Deployment + +You should update your deployment scripts to run migrations when new code is +deployed. Ideally, run migrations after the code is on your servers, but before +the application code becomes active. + +After running migrations, remember to clear the ORM cache so it renews the +column metadata of your tables. Otherwise, you might end up with errors about +columns not existing when performing operations on those new columns. The +CakePHP core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you can use: + +```bash +bin/cake migration migrate +bin/cake schema_cache clear +``` + +## Alert of Missing Migrations + +You can use the `Migrations.PendingMigrations` middleware in local development +to alert developers about new migrations that have not been applied: + +```php +use Migrations\Middleware\PendingMigrationsMiddleware; + +$config = [ + 'plugins' => [ + // Optionally include a list of plugins with migrations to check. + ], +]; + +$middlewareQueue + // ErrorHandler middleware + ->add(new PendingMigrationsMiddleware($config)); +``` + +You can add the `'app'` config key set to `false` if you are only interested in +checking plugin migrations. + +You can temporarily disable the migration check by adding +`skip-migration-check=1` to the URL query string. + +## IDE Autocomplete Support + +The [IdeHelper plugin](https://github.com/dereuromark/cakephp-ide-helper) can +help you get more IDE support for tables, their column names, and possible +column types. Specifically, PHPStorm understands the meta information and can +help you autocomplete those. diff --git a/docs/en/contents.md b/docs/en/contents.md deleted file mode 100644 index 18fe41d4c..000000000 --- a/docs/en/contents.md +++ /dev/null @@ -1,8 +0,0 @@ -### CakePHP Migrations - -- [Migrations 5.x](index) -- [Writing Migrations](writing-migrations) -- [Using the Query Builder](using-the-query-builder) -- [Executing Queries](executing-queries) -- [Database Seeding](seeding) -- [Upgrading to the builtin backend](upgrading-to-builtin-backend) diff --git a/docs/en/getting-started/creating-migrations.md b/docs/en/getting-started/creating-migrations.md new file mode 100644 index 000000000..f99f26087 --- /dev/null +++ b/docs/en/getting-started/creating-migrations.md @@ -0,0 +1,314 @@ +# Creating Migrations + +Migration files are stored in the `config/Migrations` directory of your +application. The names of the migration files are prefixed with the date in +which they were created, in the format +`YYYYMMDDHHMMSS_MigrationName.php`. + +Examples: + +- `20160121163850_CreateProducts.php` +- `20160210133047_AddRatingToProducts.php` + +The easiest way to create a migration file is by using `bin/cake bake migration`: + +```bash +bin/cake bake migration CreateProducts +``` + +This creates an empty migration that you can edit to add any columns, indexes, +and foreign keys you need. See [Writing Migrations](../guides/writing-migrations) +for more information on using `Table` objects to define schema changes. + +> [!NOTE] +> Migrations need to be applied using `bin/cake migrations migrate` after they +> have been created. + +## Migration File Names + +When generating a migration, you can follow one of the following patterns to +have additional skeleton code generated: + +- `/^(Create)(.*)/` Creates the specified table. +- `/^(Drop)(.*)/` Drops the specified table and ignores specified field arguments. +- `/^(Add).*(?:To)(.*)/` Adds fields to the specified table. +- `/^(Remove).*(?:From)(.*)/` Removes fields from the specified table. +- `/^(Alter)(.*)/` Alters the specified table. An alias for create-table and + add-field generation. +- `/^(Alter).*(?:On)(.*)/` Alters fields from the specified table. + +You can also use the `underscore_form` as the name for your migrations, such as +`create_products`. + +> [!WARNING] +> Migration names are used as class names, and thus may collide with other +> migrations if the class names are not unique. In that case, you may need to +> rename the migration manually. + +## Creating a Table + +You can use `bake migration` to create a table: + +```bash +bin/cake bake migration CreateProducts name:string description:text created modified +``` + +The command above will generate a migration file that resembles: + +```php +table('products'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('description', 'text', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->create(); + } +} +``` + +## Column Syntax + +The `bake migration` command provides a compact syntax to define columns when +generating a migration: + +```bash +bin/cake bake migration CreateProducts name:string description:text created modified +``` + +You can use the column syntax when creating tables and adding columns. You can +also edit the generated migration afterwards to customize the columns further. + +Columns on the command line follow this pattern: + +```text +fieldName:fieldType?[length]:default[value]:indexType:indexName +``` + +Examples of valid email field definitions: + +- `email:string?` +- `email:string:unique` +- `email:string?[50]` +- `email:string:unique:EMAIL_INDEX` +- `email:string[120]:unique:EMAIL_INDEX` + +While defining decimal columns, the `length` can include precision and scale: + +- `amount:decimal[5,2]` +- `amount:decimal?[5,2]` + +Columns with a question mark after the field type are nullable. + +The `length` part is optional and should always be written between brackets. + +The `default[value]` part is optional and sets the default value for the +column. Supported value types include: + +- Booleans: `true` or `false` such as `active:boolean:default[true]` +- Integers: `0`, `123`, `-456` such as `count:integer:default[0]` +- Floats: `1.5`, `-2.75` such as `rate:decimal:default[1.5]` +- Strings: `'hello'` or `"world"` such as + `status:string:default['pending']` +- Null: `null` or `NULL` such as `description:text?:default[null]` +- SQL expressions: `CURRENT_TIMESTAMP` such as + `created_at:datetime:default[CURRENT_TIMESTAMP]` + +Fields named `created` and `modified`, as well as any field with an `_at` +suffix, will automatically be set to the type `datetime`. + +There are some heuristics for choosing field types when left unspecified or set +to an invalid value. The default field type is `string`: + +- `id`: `integer` +- `created`, `modified`, `updated`: `datetime` +- `latitude`, `longitude`, `lat`, `lng`: `decimal` + +Additionally, you can create an empty migration file if you want full control +over what needs to be executed by omitting column definitions: + +```bash +bin/cake migrations create MyCustomMigration +``` + +## Adding Columns to an Existing Table + +If the migration name is of the form `AddXXXToYYY` and is followed by a list of +column names and types, a migration file containing the code for creating the +columns will be generated: + +```bash +bin/cake bake migration AddPriceToProducts price:decimal[5,2] +``` + +This generates: + +```php +table('products'); + $table->addColumn('price', 'decimal', [ + 'default' => null, + 'null' => false, + 'precision' => 5, + 'scale' => 2, + ]); + $table->update(); + } +} +``` + +## Adding a Column with an Index + +It is also possible to add indexes to columns: + +```bash +bin/cake bake migration AddNameIndexToProducts name:string:index +``` + +This will generate: + +```php +table('products'); + $table->addColumn('name', 'string') + ->addColumn('email', 'string') + ->addIndex(['name']) + // add a unique index: + ->addIndex('email', ['unique' => true]) + ->update(); + } +} +``` + +## Adding a Column with a Default Value + +You can specify default values for columns using the `default[value]` syntax: + +```bash +bin/cake bake migration AddActiveToUsers active:boolean:default[true] +``` + +This will generate: + +```php +table('users'); + $table->addColumn('active', 'boolean', [ + 'default' => true, + 'null' => false, + ]); + $table->update(); + } +} +``` + +You can combine default values with other options like nullable and indexes: + +```bash +bin/cake bake migration AddStatusToOrders status:string:default['pending']:unique +``` + +## Altering a Column + +In the same way, you can generate a migration to alter a column if the +migration name is of the form `AlterXXXOnYYY`: + +```bash +bin/cake bake migration AlterPriceOnProducts name:float +``` + +This will generate: + +```php +table('products'); + $table->changeColumn('name', 'float'); + $table->update(); + } +} +``` + +> [!WARNING] +> Changing the type of a column can result in data loss if the current and +> target column type are not compatible. For example, converting a varchar to a +> float. + +## Removing a Column + +In the same way, you can generate a migration to remove a column if the +migration name is of the form `RemoveXXXFromYYY`: + +```bash +bin/cake bake migration RemovePriceFromProducts price +``` + +This creates: + +```php +table('products'); + $table->removeColumn('price') + ->save(); + } +} +``` + +> [!NOTE] +> `removeColumn()` is not reversible, so it must be called in the `up()` +> method. Add a corresponding `addColumn()` call to the `down()` method. diff --git a/docs/en/getting-started/installation-and-overview.md b/docs/en/getting-started/installation-and-overview.md new file mode 100644 index 000000000..940a197ac --- /dev/null +++ b/docs/en/getting-started/installation-and-overview.md @@ -0,0 +1,101 @@ +# Installation and Overview + +This guide covers installation, plugin loading, and the basic migration model +used by the Migrations plugin. + +## Installation + +By default Migrations is installed with the application skeleton. If you've +removed it and want to re-install it, run the following from your application's +root directory: + +```bash +php composer.phar require cakephp/migrations "@stable" + +# Or if composer is installed globally +composer require cakephp/migrations "@stable" +``` + +To use the plugin, load it in your application's `config/bootstrap.php` file: + +```bash +bin/cake plugin load Migrations +``` + +Or load the plugin in `src/Application.php`: + +```php +$this->addPlugin('Migrations'); +``` + +Additionally, configure the default database connection in `config/app.php` as +explained in the [Database Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration). + +## Overview + +A migration is a PHP file that describes the changes to apply to your database. +A migration file can add, change, or remove tables, columns, indexes, and +foreign keys. + +If we wanted to create a table, we could use a migration similar to this: + +```php +table('products'); + $table->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => false, + ]); + $table->addColumn('description', 'text', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('created', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->addColumn('modified', 'datetime', [ + 'default' => null, + 'null' => false, + ]); + $table->create(); + } +} +``` + +When applied, this migration will add a table to your database named +`products` with the following column definitions: + +- `id` column of type `integer` as primary key. This column is added + implicitly, but you can customize the name and type if necessary. +- `name` column of type `string` +- `description` column of type `text` +- `created` column of type `datetime` +- `modified` column of type `datetime` + +> [!NOTE] +> Migrations are not automatically applied. Use the CLI commands to apply and +> roll back migrations. + +Once the file has been created in the `config/Migrations` folder, you can apply +it: + +```bash +bin/cake migrations migrate +``` + +## Next Steps + +- Use [Creating Migrations](creating-migrations) to generate migration files + with `bake` +- Use [Running and Managing Migrations](running-and-managing-migrations) to + apply, roll back, and inspect migrations +- Use [Writing Migrations](../guides/writing-migrations) for the full Table API and + migration authoring reference diff --git a/docs/en/getting-started/running-and-managing-migrations.md b/docs/en/getting-started/running-and-managing-migrations.md new file mode 100644 index 000000000..f3c98fda8 --- /dev/null +++ b/docs/en/getting-started/running-and-managing-migrations.md @@ -0,0 +1,135 @@ +# Running and Managing Migrations + +This guide covers the commands you use after writing or generating migration +files. + +## Applying Migrations + +Once you have generated or written your migration file, apply the changes to +your database: + +```bash +# Run all the migrations +bin/cake migrations migrate + +# Migrate to a specific version using the --target option +bin/cake migrations migrate -t 20150103081132 + +# Run migrations from a custom source directory +bin/cake migrations migrate -s Alternate + +# Run migrations against a different connection +bin/cake migrations migrate -c my_custom_connection + +# Run migrations for a plugin +bin/cake migrations migrate -p MyAwesomePlugin +``` + +## Reverting Migrations + +The `rollback` command undoes previously executed migrations: + +```bash +# Roll back to the previous migration +bin/cake migrations rollback + +# Roll back to a specific version +bin/cake migrations rollback -t 20150103081132 +``` + +You can also use the `--source`, `--connection`, and `--plugin` options just +like for the `migrate` command. + +## Viewing Migration Status + +The `status` command prints a list of all migrations, along with their current +status: + +```bash +bin/cake migrations status +``` + +You can also output the results as JSON: + +```bash +bin/cake migrations status --format json +``` + +You can also use the `--source`, `--connection`, and `--plugin` options just +like for the `migrate` command. + +### Cleaning Up Missing Migrations + +Sometimes migration files may be deleted from the filesystem but still exist in +the migrations tracking table. These migrations will be marked as `MISSING` in +the status output. You can remove these entries using the `--cleanup` option: + +```bash +bin/cake migrations status --cleanup +``` + +This will remove all migration entries from the tracking table that no longer +have corresponding migration files in the filesystem. + +## Marking a Migration as Migrated + +It can sometimes be useful to mark a set of migrations as migrated without +actually running them. In order to do this, use the `mark_migrated` command. + +You can mark all migrations as migrated: + +```bash +bin/cake migrations mark_migrated +``` + +You can also mark all migrations up to a specific version using the `--target` +option: + +```bash +bin/cake migrations mark_migrated --target=20151016204000 +``` + +If you do not want the targeted migration to be marked as migrated during the +process, use the `--exclude` flag: + +```bash +bin/cake migrations mark_migrated --target=20151016204000 --exclude +``` + +If you wish to mark only the targeted migration as migrated, use the `--only` +flag: + +```bash +bin/cake migrations mark_migrated --target=20151016204000 --only +``` + +You can also use the `--source`, `--connection`, and `--plugin` options just +like for the `migrate` command. + +> [!NOTE] +> When you bake a snapshot with `cake bake migration_snapshot`, the created +> migration will automatically be marked as migrated. To prevent this behavior, +> for example for unit test migrations, use the `--generate-only` flag. + +This command also accepts the migration version number as a positional +argument: + +```bash +bin/cake migrations mark_migrated 20150420082532 +``` + +If you wish to mark all migrations as migrated, you can use the `all` special +value: + +```bash +bin/cake migrations mark_migrated all +``` + +## Seeding Your Database + +Seed classes are a good way to populate your database with default or starter +data. They are also useful for generating data for development environments. + +By default, seeds are looked for in the `config/Seeds/` directory of your +application. See [Database Seeding](../guides/seeding) for how to build and use seed +classes. diff --git a/docs/en/getting-started/snapshots-and-diffs.md b/docs/en/getting-started/snapshots-and-diffs.md new file mode 100644 index 000000000..a5b8f1d9c --- /dev/null +++ b/docs/en/getting-started/snapshots-and-diffs.md @@ -0,0 +1,116 @@ +# Snapshots and Diffs + +This guide covers bootstrapping migrations from an existing database and +generating migration diffs from schema changes. + +## Generating Migration Snapshots from an Existing Database + +If you have a pre-existing database and want to start using migrations, or want +to version-control the initial schema of your application, run +`bake migration_snapshot`: + +```bash +bin/cake bake migration_snapshot Initial +``` + +It will generate a migration file called `YYYYMMDDHHMMSS_Initial.php` +containing all the create statements for all tables in your database. + +By default, the snapshot will be created by connecting to the database defined +in the `default` connection configuration. If you need to bake a snapshot from +a different datasource, use the `--connection` option: + +```bash +bin/cake bake migration_snapshot Initial --connection my_other_connection +``` + +You can also make sure the snapshot includes only the tables for which you have +defined the corresponding model classes by using the `--require-table` flag: + +```bash +bin/cake bake migration_snapshot Initial --require-table +``` + +When using `--require-table`, the shell will look through your application's +`Table` classes and will only add the model tables to the snapshot. + +If you want to generate a snapshot without marking it as migrated, for example +for use in unit tests, use the `--generate-only` flag: + +```bash +bin/cake bake migration_snapshot Initial --generate-only +``` + +This will create the migration file but will not add an entry to the migrations +tracking table, allowing you to move the file to a different location without +causing `MISSING` status issues. + +To bake a snapshot for a plugin, use the `--plugin` option: + +```bash +bin/cake bake migration_snapshot Initial --plugin MyPlugin +``` + +Only the tables that have a `Table` object model class defined will be added to +the snapshot of your plugin. + +> [!NOTE] +> When baking a snapshot for a plugin, the migration files will be created in +> your plugin's `config/Migrations` directory. + +Be aware that when you bake a snapshot, it is automatically added to the +migrations log table as migrated unless you use `--generate-only`. + +## Generating a Diff + +As migrations are applied and rolled back, the migrations plugin will generate +a dump file of your schema. If you make manual changes to your database schema +outside of migrations, you can use `bake migration_diff` to generate a +migration file that captures the difference between the current schema dump file +and the database schema: + +```bash +bin/cake bake migration_diff NameOfTheMigrations +``` + +By default, the diff will be created by connecting to the database defined in +the `default` connection configuration. If you need to bake a diff from a +different datasource, use the `--connection` option: + +```bash +bin/cake bake migration_diff NameOfTheMigrations --connection my_other_connection +``` + +If you want to use the diff feature on an application that already has a +migrations history, you need to manually create the dump file that will be used +as comparison: + +```bash +bin/cake migrations dump +``` + +The database state must be the same as it would be if you had just migrated all +your migrations before you create a dump file. Once the dump file is generated, +you can start doing changes in your database and use +`bake migration_diff` whenever you need to capture them. + +> [!NOTE] +> Migration diff generation cannot detect column renamings. + +## Generating a Dump File + +The dump command creates a file to be used with `bake migration_diff`: + +```bash +bin/cake migrations dump +``` + +Each generated dump file is specific to the connection it is generated from, +and is suffixed as such. This allows `bake migration_diff` to properly compute +diffs when your application is dealing with multiple databases, possibly from +different vendors. + +Dump files are created in the same directory as your migration files. + +You can also use the `--source`, `--connection`, and `--plugin` options just +like for the `migrate` command. diff --git a/docs/en/executing-queries.md b/docs/en/guides/executing-queries.md similarity index 60% rename from docs/en/executing-queries.md rename to docs/en/guides/executing-queries.md index 3c6c3edd3..6f3d605ce 100644 --- a/docs/en/executing-queries.md +++ b/docs/en/guides/executing-queries.md @@ -76,60 +76,3 @@ class MyNewMigration extends BaseMigration } } ``` - -## Inserting Data - -Migrations makes it easy to insert data into your tables. Whilst this feature is -intended for the [seed feature](seeding), you are also free to use the -insert methods in your migrations: - -```php -table('status'); - - // inserting only one row - $singleRow = [ - 'id' => 1, - 'name' => 'In Progress' - ]; - - $table->insert($singleRow)->saveData(); - - // inserting multiple rows - $rows = [ - [ - 'id' => 2, - 'name' => 'Stopped' - ], - [ - 'id' => 3, - 'name' => 'Queued' - ] - ]; - - $table->insert($rows)->saveData(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $this->execute('DELETE FROM status'); - } -} -``` - -> [!NOTE] -> You cannot use the insert methods inside a change() method. Please use the -> up() and down() methods. diff --git a/docs/en/guides/inserting-data.md b/docs/en/guides/inserting-data.md new file mode 100644 index 000000000..d9661c035 --- /dev/null +++ b/docs/en/guides/inserting-data.md @@ -0,0 +1,56 @@ +# Inserting Data + +Migrations makes it easy to insert data into your tables. Whilst this feature +is intended for the [seed feature](seeding), you are also free to use the +insert methods in your migrations: + +```php +table('status'); + + // inserting only one row + $singleRow = [ + 'id' => 1, + 'name' => 'In Progress', + ]; + + $table->insert($singleRow)->saveData(); + + // inserting multiple rows + $rows = [ + [ + 'id' => 2, + 'name' => 'Stopped', + ], + [ + 'id' => 3, + 'name' => 'Queued', + ], + ]; + + $table->insert($rows)->saveData(); + } + + /** + * Migrate Down. + */ + public function down(): void + { + $this->execute('DELETE FROM status'); + } +} +``` + +> [!NOTE] +> You cannot use insert methods inside `change()`. Use `up()` and `down()` +> instead. diff --git a/docs/en/seeding.md b/docs/en/guides/seeding.md similarity index 100% rename from docs/en/seeding.md rename to docs/en/guides/seeding.md diff --git a/docs/en/using-the-query-builder.md b/docs/en/guides/using-the-query-builder.md similarity index 100% rename from docs/en/using-the-query-builder.md rename to docs/en/guides/using-the-query-builder.md diff --git a/docs/en/guides/writing-migrations/columns-and-table-operations.md b/docs/en/guides/writing-migrations/columns-and-table-operations.md new file mode 100644 index 000000000..c8bd82baf --- /dev/null +++ b/docs/en/guides/writing-migrations/columns-and-table-operations.md @@ -0,0 +1,388 @@ +# Columns and Table Operations + + + +## Adding Columns + +Column types are specified as strings and can be one of: + +- binary +- boolean +- char +- date +- datetime +- decimal +- float +- double +- smallinteger +- integer +- biginteger +- string +- text +- time +- timestamp +- uuid +- binaryuuid +- nativeuuid + +In addition, the MySQL adapter supports `enum`, `set`, `blob`, `tinyblob`, +`mediumblob`, `longblob`, `bit` and `json` column types (`json` in MySQL 5.7 +and above). When providing a limit value and using `binary`, `varbinary` or +`blob` and its subtypes, the retained column type will be based on required +length (see [Limit Option and MySQL](#limit-option-and-mysql) for details). + +With most adapters, the `uuid` and `nativeuuid` column types are aliases, +however with the MySQL adapter + MariaDB, the `nativeuuid` type maps to a +native UUID column instead of `CHAR(36)` like `uuid` does. + +In addition, the Postgres adapter supports `interval`, `json`, `jsonb`, `uuid`, +`cidr`, `inet` and `macaddr` column types (PostgreSQL 9.3 and above). + +### Valid Column Options + +The following are valid column options. + +For any column type: + +| Option | Description | +|---------|----------------------------------------------------------------------------| +| limit | set maximum length for strings, also hints column types in adapters | +| length | alias for `limit` | +| default | set default value or action | +| null | allow `NULL` values, defaults to `true` | +| after | specify the column that a new column should be placed after *(MySQL only)* | +| comment | set a text comment on the column | + +For `decimal` and `float` columns: + +| Option | Description | +|-----------|--------------------------------------------------------| +| precision | total number of digits | +| scale | number of digits after the decimal point | +| signed | enable or disable the `unsigned` option *(MySQL only)* | + +> [!NOTE] +> Migrations follows the SQL standard where `precision` is the total number of +> digits and `scale` is digits after the decimal point. + +For `enum` and `set` columns: + +| Option | Description | +|--------|--------------------------------------------| +| values | comma separated list or an array of values | + +For `smallinteger`, `integer` and `biginteger` columns: + +| Option | Description | +|----------|--------------------------------------------------------| +| identity | enable or disable automatic incrementing | +| signed | enable or disable the `unsigned` option *(MySQL only)* | + +For `date`, `time`, `datetime`, and `timestamp` columns, default/timezone/update +options are supported as appropriate for the adapter. For `string` and `text`, +MySQL also supports `collation` and `encoding`. + +You can add `created` and `updated` timestamps using `addTimestamps()` or +`addTimestampsWithTimezone()`: + +```php +$this->table('users')->addTimestamps()->create(); +$this->table('users')->addTimestampsWithTimezone()->create(); +``` + +### Limit Option and MySQL + +When using the MySQL adapter, there are a couple things to consider when +working with limits: + +- When using a `string` primary key or index on MySQL 5.7 or below, or the + MyISAM storage engine, and the default charset of `utf8mb4_unicode_ci`, you + must specify a limit less than or equal to 191, or use a different charset. +- Additional hinting of database column type can be made for `integer`, `text`, + `blob`, `tinyblob`, `mediumblob`, `longblob` columns. + +```php +table('cart_items'); +$table->addColumn('user_id', 'integer') + ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) + ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) + ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) + ->create(); +``` + +### Default Values with Expressions + +If you need to set a default to an expression, you can use a `Literal` to have +the column's default value used without quoting or escaping: + +```php +use Migrations\BaseMigration; +use Migrations\Db\Literal; + +class AddSomeColumns extends BaseMigration +{ + public function change(): void + { + $this->table('users') + ->addColumn('uniqid', 'uuid', [ + 'default' => Literal::from('uuid_generate_v4()'), + ]) + ->create(); + } +} +``` + +## Creating a Table + +Creating a table is straightforward using the Table object: + +```php +table('users'); + $users->addColumn('username', 'string', ['limit' => 20]) + ->addColumn('password', 'string', ['limit' => 40]) + ->addColumn('email', 'string', ['limit' => 100]) + ->addColumn('created', 'datetime') + ->addColumn('updated', 'datetime', ['null' => true]) + ->addIndex(['username', 'email'], ['unique' => true]) + ->create(); + } +} +``` + +Migrations automatically creates an auto-incrementing primary key column called +`id` for every table unless configured otherwise. + +You can disable the automatic `id` column and define a custom primary key: + +```php +$table = $this->table('followers', [ + 'id' => false, + 'primary_key' => ['user_id', 'follower_id'], +]); +``` + +The MySQL adapter also supports table options such as `comment`, `collation`, +`row_format`, `engine`, `signed`, and `limit`. + +To set unsigned primary keys, pass the `signed` option with `false`, or enable +the `Migrations.unsigned_primary_keys` and `Migrations.unsigned_ints` feature +flags together. See [Feature Flags](../../advanced/integration-and-deployment#feature-flags). + +## MySQL ALTER TABLE Options + +When modifying tables in MySQL, you can control how the `ALTER TABLE` +operation is performed using the `algorithm` and `lock` options: + +```php +$table->update([ + 'algorithm' => 'INPLACE', + 'lock' => 'NONE', +]); +``` + +Not all operations support all `algorithm` and `lock` combinations, and MySQL +will raise an error if the requested combination is not possible. + +## Table Partitioning + +Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning +helps manage large tables by splitting them into smaller, more manageable +pieces. + +Supported strategies include: + +- `RANGE` +- `RANGE COLUMNS` +- `LIST` +- `LIST COLUMNS` +- `HASH` +- `KEY` *(MySQL only)* + +You can also partition by expressions using `Literal::from(...)`, and add or +drop partitions on existing tables. + +## Saving Changes + +When working with the Table object, Migrations stores certain operations in a +pending changes cache. Once you have made the changes you want to the table, +you must save them. Migrations provides three methods: + +- `create()` creates the table and runs pending changes +- `update()` runs pending changes on an existing table +- `save()` creates or updates depending on whether the table exists + +When using `change()`, you should always use `create()` or `update()`, and +never `save()`. + +## Renaming a Column + +To rename a column, call `renameColumn()`: + +```php +$this->table('users') + ->renameColumn('bio', 'biography') + ->save(); +``` + +## Adding a Column After Another Column + +When adding a column with the MySQL adapter, you can dictate its position using +the `after` option: + +```php +$this->table('users') + ->addColumn('city', 'string', ['after' => 'email']) + ->update(); +``` + +The `\Migrations\Db\Adapter\MysqlAdapter::FIRST` constant can be used to place +the new column first. + +## Dropping a Column + +To drop a column, use `removeColumn()`: + +```php +$this->table('users') + ->removeColumn('short_name') + ->save(); +``` + +## Specifying a Column Limit + +You can limit the maximum length of a column using the `limit` option: + +```php +$this->table('tags') + ->addColumn('short_name', 'string', ['limit' => 30]) + ->update(); +``` + +## Changing Column Attributes + +There are two methods for modifying existing columns: + +### Updating Columns + +To modify specific column attributes while preserving others, use +`updateColumn()`: + +```php +$this->table('users') + ->updateColumn('email', null, ['null' => true]) + ->save(); +``` + +This automatically preserves unspecified attributes such as defaults, +nullability, limits, comments, signedness, collation, and enum/set values. + +### Changing Columns + +To completely replace a column definition, use `changeColumn()` and specify all +desired attributes: + +```php +$this->table('users') + ->changeColumn('email', 'string', [ + 'limit' => 255, + 'null' => true, + 'default' => null, + ]) + ->save(); +``` + +You can enable attribute preservation with `'preserveUnspecified' => true`. + +## Determining Whether a Table Exists + +Use `hasTable()` to check whether a table exists: + +```php +if ($this->hasTable('users')) { + // do something +} +``` + +## Dropping a Table + +Tables can be dropped using `drop()`: + +```php +$this->table('users')->drop()->save(); +``` + +## Renaming a Table + +To rename a table, call `rename()`: + +```php +$this->table('users') + ->rename('legacy_users') + ->update(); +``` + +## Changing the Primary Key + +To change the primary key on an existing table, use `changePrimaryKey()`: + +```php +$this->table('users') + ->changePrimaryKey(['new_id', 'username']) + ->save(); +``` + +## Creating Custom Primary Keys + +You can specify an `autoId` property in the migration class and set it to +`false`, which turns off automatic `id` column creation: + +```php +table('products') + ->addColumn('id', 'uuid') + ->addPrimaryKey('id') + ->addColumn('name', 'string') + ->addColumn('description', 'text') + ->create(); + } +} +``` + +> [!WARNING] +> Dealing with primary keys can only be done on table creation operations for +> some database servers. + +## Changing the Table Comment + +To change the comment on an existing table, use `changeComment()`: + +```php +$this->table('users') + ->changeComment('This is the table with users auth information') + ->save(); +``` + +## Next Steps + +- [Indexes and Constraints](indexes-and-constraints) +- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations) diff --git a/docs/en/guides/writing-migrations/index.md b/docs/en/guides/writing-migrations/index.md new file mode 100644 index 000000000..7d25425c4 --- /dev/null +++ b/docs/en/guides/writing-migrations/index.md @@ -0,0 +1,21 @@ +# Writing Migrations + +Migrations are a declarative API that helps you transform your database. Each +migration is represented by a PHP class in a unique file. It is preferred that +you write your migrations using the Migrations API, but raw SQL is also +supported. + +For generating migration files with `bake`, naming patterns, anonymous +migration classes, and command-line column syntax, see +[Creating Migrations](../../getting-started/creating-migrations). + +## Guide Map + +This section has been split into focused reference pages: + +- [Migration Methods](migration-methods) +- [Columns and Table Operations](columns-and-table-operations) +- [Indexes and Constraints](indexes-and-constraints) +- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations) + +Use these pages as the API reference for authoring migrations. diff --git a/docs/en/guides/writing-migrations/indexes-and-constraints.md b/docs/en/guides/writing-migrations/indexes-and-constraints.md new file mode 100644 index 000000000..5c7a680db --- /dev/null +++ b/docs/en/guides/writing-migrations/indexes-and-constraints.md @@ -0,0 +1,148 @@ +# Indexes and Constraints + +## Working With Indexes + +To add an index to a table, call `addIndex()`: + +```php +table('users') + ->addColumn('city', 'string') + ->addIndex(['city']) + ->save(); + } +} +``` + +You can also specify unique indexes, explicit names, sort order, index length, +and advanced adapter-specific options. + +The fluent builder is also available: + +```php +$this->table('users') + ->addIndex( + $this->index(['email', 'username']) + ->setType('unique') + ->setName('idx_users_email') + ->setOrder(['email' => 'DESC', 'username' => 'ASC']) + ) + ->save(); +``` + +Adapter-specific capabilities include: + +- MySQL `fulltext` indexes +- MySQL index-length options +- SQL Server and PostgreSQL `include` columns +- PostgreSQL, SQL Server, and SQLite partial indexes +- PostgreSQL concurrent index creation +- PostgreSQL `gin` indexes + +To remove indexes, use `removeIndex()` or `removeIndexByName()`. + +## Working With Foreign Keys + +Migrations supports foreign key constraints on database tables: + +```php +table('tag_relationships') + ->addColumn('tag_id', 'integer', ['null' => true]) + ->addForeignKey( + 'tag_id', + 'tags', + 'id', + ['delete' => 'SET_NULL', 'update' => 'NO_ACTION'] + ) + ->save(); + } +} +``` + +The `delete` and `update` options control `ON DELETE` and `ON UPDATE` +behavior. Valid values are `SET_NULL`, `NO_ACTION`, `CASCADE`, and +`RESTRICT`. + +Foreign keys can also be defined with arrays of columns for composite keys. + +The `foreignKey()` fluent builder is available for more complex cases: + +```php +$this->table('articles') + ->addForeignKey( + $this->foreignKey() + ->setColumns('user_id') + ->setReferencedTable('users') + ->setReferencedColumns('user_id') + ->setName('article_user_fk') + ) + ->save(); +``` + +Use `hasForeignKey()` to check whether a foreign key exists, and +`dropForeignKey()` to remove one. + +## Working With Check Constraints + +Check constraints allow you to enforce data validation rules at the database +level. + +> [!NOTE] +> Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite. + +### Adding a Check Constraint + +```php +$this->table('products') + ->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) + ->addCheckConstraint('price_positive', 'price > 0') + ->save(); +``` + +### Using the Fluent Builder + +```php +$this->table('users') + ->addCheckConstraint( + $this->checkConstraint() + ->setName('age_valid') + ->setExpression('age >= 18 AND age <= 120') + ) + ->save(); +``` + +If you do not specify a name, one will be auto-generated. + +Check constraints can reference multiple columns and use more complex SQL +expressions. + +Use `hasCheckConstraint()` to verify existence and `dropCheckConstraint()` to +remove a constraint. + +### Database-Specific Behavior + +- MySQL stores check constraint metadata in + `INFORMATION_SCHEMA.CHECK_CONSTRAINTS` +- PostgreSQL stores constraints in `pg_constraint` +- SQLite recreates the table when altering check constraints +- SQL Server support is planned for a future release + +## Next Steps + +- [Columns and Table Operations](columns-and-table-operations) +- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations) diff --git a/docs/en/guides/writing-migrations/migration-methods.md b/docs/en/guides/writing-migrations/migration-methods.md new file mode 100644 index 000000000..2ac07c995 --- /dev/null +++ b/docs/en/guides/writing-migrations/migration-methods.md @@ -0,0 +1,135 @@ +# Migration Methods + +## The Change Method + +Migrations supports 'reversible migrations'. In many scenarios, you only need +to define the `up` logic, and Migrations can figure out how to generate the +rollback operations for you. For example: + +```php +table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + } +} +``` + +When executing this migration, Migrations will create the `user_logins` table +on the way up and automatically figure out how to drop the table on the way +down. Please be aware that when a `change` method exists, Migrations will +ignore the `up` and `down` methods. If you need to use these methods it is +recommended to create a separate migration file. + +> [!NOTE] +> When creating or updating tables inside a `change()` method you must use the +> Table `create()` and `update()` methods. Migrations cannot automatically +> determine whether a `save()` call is creating a new table or modifying an +> existing one. + +The following actions are reversible when done through the Table API in +Migrations, and will be automatically reversed: + +- Creating a table +- Renaming a table +- Adding a column +- Renaming a column +- Adding an index +- Adding a foreign key +- Adding a check constraint + +If a command cannot be reversed then Migrations will throw an +`IrreversibleMigrationException` when it's migrating down. If you wish to use a +command that cannot be reversed in the change function, you can use an `if` +statement with `$this->isMigratingUp()` to only run things in the up or down +direction. For example: + +```php +table('user_logins'); + $table->addColumn('user_id', 'integer') + ->addColumn('created', 'datetime') + ->create(); + if ($this->isMigratingUp()) { + $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) + ->save(); + } + } +} +``` + +## The Up Method + +The `up()` method is automatically run by Migrations when you are migrating up +and it detects the given migration hasn't been executed previously. You should +use the `up()` method to transform the database with your intended changes. + +## The Down Method + +The `down()` method is automatically run by Migrations when you are migrating +down and it detects the given migration has been executed in the past. You +should use the `down()` method to reverse the transformations described in the +`up()` method. + +## The Init Method + +The `init()` method is run by Migrations before the migration methods if it +exists. This can be used for setting common class properties that are then used +within the migration methods. + +## The Should Execute Method + +The `shouldExecute()` method is run by Migrations before executing the +migration. This can be used to prevent the migration from being executed at +this time. It always returns `true` by default. You can override it in your +custom `BaseMigration` implementation. + +## Working With Tables + +The Table object enables you to manipulate database tables using PHP code. You +can retrieve an instance of the Table object by calling the `table()` method +from within your database migration: + +```php +table('tableName'); + } + + public function down(): void + { + } +} +``` + +You can then manipulate this table using the methods provided by the Table +object. + +## Next Steps + +- [Columns and Table Operations](columns-and-table-operations) +- [Indexes and Constraints](indexes-and-constraints) +- [Schema Introspection and Platform Limitations](schema-introspection-and-platform-limitations) diff --git a/docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md b/docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md new file mode 100644 index 000000000..d733a9c9d --- /dev/null +++ b/docs/en/guides/writing-migrations/schema-introspection-and-platform-limitations.md @@ -0,0 +1,105 @@ +# Schema Introspection and Platform Limitations + +## Checking Columns + +`BaseMigration` also provides methods for introspecting the current schema, +allowing you to conditionally make changes to schema, or read data. Schema is +inspected when the migration is run. + +### Get a Column List + +To retrieve all table columns, create a table object and call `getColumns()`: + +```php +table('users')->getColumns(); + } +} +``` + +### Get a Column by Name + +To retrieve one table column, call `getColumn()`: + +```php +table('users')->getColumn('email'); + } +} +``` + +### Check Whether a Column Exists + +Use `hasColumn()` to determine whether a table already has a given column: + +```php +table('user'); + if ($table->hasColumn('username')) { + // do something + } + } +} +``` + +## Changing Templates + +See [Custom Seed Migration Templates](../seeding#custom-seed-migration-templates) +for how to customize the templates used to generate migrations. + +## Database-Specific Limitations + +While Migrations aims to provide a database-agnostic API, some features have +database-specific limitations or are not available on all platforms. + +### SQL Server + +The following features are not supported on SQL Server: + +- Check constraints are not currently implemented +- Table comments are not supported +- `insertOrSkip()` is not supported; use `insertOrUpdate()` instead + +### SQLite + +SQLite limitations include: + +- named foreign keys are not supported +- table comments are stored as metadata, not in the database itself +- altering check constraints requires recreating the table +- table partitioning is not supported + +### PostgreSQL + +PostgreSQL does not support MySQL's `KEY` partitioning type. Use `HASH` +partitioning instead for similar distribution behavior. + +### MySQL/MariaDB + +For MySQL, the `$conflictColumns` parameter in `insertOrUpdate()` is ignored +because MySQL's `ON DUPLICATE KEY UPDATE` automatically applies to all unique +constraints. PostgreSQL and SQLite require this parameter to be specified. + +Some geometry column features may not work correctly on MariaDB due to +differences in GIS implementation compared to MySQL. diff --git a/docs/en/index.md b/docs/en/index.md index 2126603b6..7c5c13c0d 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -14,20 +14,22 @@ Migrations 5.x includes several new features: - **Seed tracking** - Seeds are now tracked in a `cake_seeds` table, preventing accidental re-runs -- **Check constraints** - Support for database check constraints via `addCheckConstraint()` +- **Check constraints** - Support for database check constraints via + `addCheckConstraint()` - **Default values in bake** - Specify default values when baking migrations (e.g., `active:boolean:default[true]`) -- **MySQL ALTER options** - Control `ALGORITHM` and `LOCK` for ALTER TABLE operations +- **MySQL ALTER options** - Control `ALGORITHM` and `LOCK` for ALTER TABLE + operations - **insertOrSkip()** - New method for idempotent seed data insertion -See the [Upgrading from 4.x to 5.x](upgrading) guide for breaking changes and migration steps from 4.x. +See the [Upgrading from 4.x to 5.x](upgrades/upgrading-from-4-x) guide for breaking changes and +migration steps from 4.x. ## Installation -By default Migrations is installed with the application skeleton. If -you've removed it and want to re-install it, you can do so by running the -following from your application's ROOT directory (where **composer.json** file is -located): +By default Migrations is installed with the application skeleton. If you've +removed it and want to re-install it, run the following from your application's +root directory: ```bash php composer.phar require cakephp/migrations "@stable" @@ -36,34 +38,26 @@ php composer.phar require cakephp/migrations "@stable" composer require cakephp/migrations "@stable" ``` -To use the plugin you'll need to load it in your application's -**config/bootstrap.php** file. You can use [CakePHP's Plugin shell](https://book.cakephp.org/5/en/console-and-shells/plugin-shell.html) to -load and unload plugins from your **config/bootstrap.php**: +To use the plugin, load it in your application's `config/bootstrap.php` file: ```bash bin/cake plugin load Migrations ``` -Or you can load the plugin by editing your **src/Application.php** file and -adding the following statement: +Or load the plugin in `src/Application.php`: ```php $this->addPlugin('Migrations'); ``` -Additionally, you will need to configure the default database configuration for -your application in your **config/app.php** file as explained in the [Database -Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration). - -## Upgrading from 4.x - -If you are upgrading from Migrations 4.x, please see the [Upgrading from 4.x to 5.x](upgrading) guide -for breaking changes and migration steps. +Additionally, configure the default database connection in `config/app.php` as +explained in the [Database Configuration section](https://book.cakephp.org/5/en/orm/database-basics.html#database-configuration). ## Overview A migration is a PHP file that describes the changes to apply to your database. -A migration file can add, change or remove tables, columns, indexes and foreign keys. +A migration file can add, change, or remove tables, columns, indexes, and +foreign keys. If we wanted to create a table, we could use a migration similar to this: @@ -109,833 +103,114 @@ When applied, this migration will add a table to your database named - `modified` column of type `datetime` > [!NOTE] -> Migrations are not automatically applied, you can apply and rollback -> migrations with CLI commands. - -Once the file has been created in the **config/Migrations** folder, you can -apply it: +> Migrations are not automatically applied. Use the CLI commands to apply and +> roll back migrations. ```bash bin/cake migrations migrate ``` -## Creating Migrations - -Migration files are stored in the **config/Migrations** directory of your -application. The name of the migration files are prefixed with the date in -which they were created, in the format **YYYYMMDDHHMMSS_MigrationName.php**. -Here are examples of migration filenames: - -- **20160121163850_CreateProducts.php** -- **20160210133047_AddRatingToProducts.php** - -The easiest way to create a migrations file is by using `bin/cake bake migration` CLI command: - -```bash -bin/cake bake migration CreateProducts -``` - -This will create an empty migration that you can edit to add any columns, -indexes and foreign keys you need. See the [Creating A Table](writing-migrations#creating-a-table) section to -learn more about using migrations to define tables. - -> [!NOTE] -> Migrations need to be applied using `bin/cake migrations migrate` after -> they have been created. - -### Migration file names - -When generating a migration, you can follow one of the following patterns -to have additional skeleton code generated: - -- `/^(Create)(.*)/` Creates the specified table. -- `/^(Drop)(.*)/` Drops the specified table. - Ignores specified field arguments -- `/^(Add).*(?:To)(.*)/` Adds fields to the specified - table -- `/^(Remove).*(?:From)(.*)/` Removes fields from the - specified table -- `/^(Alter)(.*)/` Alters the specified table. An alias - for CreateTable and AddField. -- `/^(Alter).*(?:On)(.*)/` Alters fields from the specified table. - -You can also use the `underscore_form` as the name for your migrations i.e. -`create_products`. - -> [!WARNING] -> Migration names are used as class names, and thus may collide with -> other migrations if the class names are not unique. In this case, it may be -> necessary to manually override the name at a later date, or simply change -> the name you are specifying. - -### Creating a table - -You can use `bake migration` to create a table: - -```bash -bin/cake bake migration CreateProducts name:string description:text created modified -``` - -The command line above will generate a migration file that resembles: - -```php -table('products'); - $table->addColumn('name', 'string', [ - 'default' => null, - 'limit' => 255, - 'null' => false, - ]); - $table->addColumn('description', 'text', [ - 'default' => null, - 'null' => false, - ]); - $table->addColumn('created', 'datetime', [ - 'default' => null, - 'null' => false, - ]); - $table->addColumn('modified', 'datetime', [ - 'default' => null, - 'null' => false, - ]); - $table->create(); - } -} -``` - -### Column syntax - -The `bake migration` command provides a compact syntax to define columns when -generating a migration: - -```bash -bin/cake bake migration CreateProducts name:string description:text created modified -``` - -You can use the column syntax when creating tables and adding columns. You can -also edit the migration after generation to add or customize the columns - -Columns on the command line follow the following pattern: - - fieldName:fieldType?[length]:default[value]:indexType:indexName - -For instance, the following are all valid ways of specifying an email field: - -- `email:string?` -- `email:string:unique` -- `email:string?[50]` -- `email:string:unique:EMAIL_INDEX` -- `email:string[120]:unique:EMAIL_INDEX` - -While defining decimal columns, the `length` can be defined to have precision -and scale, separated by a comma. - -- `amount:decimal[5,2]` -- `amount:decimal?[5,2]` - -Columns with a question mark after the fieldType will make the column nullable. - -The `length` part is optional and should always be written between bracket. - -The `default[value]` part is optional and sets the default value for the column. -Supported value types include: - -- Booleans: `true` or `false` - e.g., `active:boolean:default[true]` -- Integers: `0`, `123`, `-456` - e.g., `count:integer:default[0]` -- Floats: `1.5`, `-2.75` - e.g., `rate:decimal:default[1.5]` -- Strings: `'hello'` or `"world"` (quoted) - e.g., `status:string:default['pending']` -- Null: `null` or `NULL` - e.g., `description:text?:default[null]` -- SQL expressions: `CURRENT_TIMESTAMP` - e.g., `created_at:datetime:default[CURRENT_TIMESTAMP]` - -Fields named `created` and `modified`, as well as any field with a `_at` -suffix, will automatically be set to the type `datetime`. - -There are some heuristics to choosing fieldtypes when left unspecified or set to -an invalid value. Default field type is `string`: - -- id: integer -- created, modified, updated: datetime -- latitude, longitude (or short forms lat, lng): decimal - -Additionally you can create an empty migrations file if you want full control -over what needs to be executed, by omitting to specify a columns definition: - -```bash -bin/cake migrations create MyCustomMigration -``` - -See [Writing Migrations](writing-migrations) for more information on how to use `Table` -objects to interact with tables and define schema changes. - -### Adding columns to an existing table - -If the migration name in the command line is of the form "AddXXXToYYY" and is -followed by a list of column names and types then a migration file containing -the code for creating the columns will be generated: - -```bash -bin/cake bake migration AddPriceToProducts price:decimal[5,2] -``` - -Executing the command line above will generate: - -```php -table('products'); - $table->addColumn('price', 'decimal', [ - 'default' => null, - 'null' => false, - 'precision' => 5, - 'scale' => 2, - ]); - $table->update(); - } -} -``` - -### Adding a column with an index - -It is also possible to add indexes to columns: - -```bash -bin/cake bake migration AddNameIndexToProducts name:string:index -``` - -will generate: - -```php -table('products'); - $table->addColumn('name', 'string') - ->addColumn('email', 'string') - ->addIndex(['name']) - // add a unique index: - ->addIndex('email', ['unique' => true]) - ->update(); - } -} -``` - -### Adding a column with a default value +## Guide Map -You can specify default values for columns using the `default[value]` syntax: +Use the focused guides below instead of a single long reference page: -```bash -bin/cake bake migration AddActiveToUsers active:boolean:default[true] -``` - -will generate: +- [Installation and Overview](getting-started/installation-and-overview) +- [Creating Migrations](getting-started/creating-migrations) +- [Snapshots and Diffs](getting-started/snapshots-and-diffs) +- [Running and Managing Migrations](getting-started/running-and-managing-migrations) +- [Integration and Deployment](advanced/integration-and-deployment) +- [Writing Migrations](guides/writing-migrations) +- [Using the Query Builder](guides/using-the-query-builder) +- [Executing Queries](guides/executing-queries) +- [Database Seeding](guides/seeding) +- [Upgrading to the builtin backend](upgrades/upgrading-to-builtin-backend) -```php -table('users'); - $table->addColumn('active', 'boolean', [ - 'default' => true, - 'null' => false, - ]); - $table->update(); - } -} -``` - -You can combine default values with other options like nullable and indexes: - -```bash -bin/cake bake migration AddStatusToOrders status:string:default['pending']:unique -``` - -### Altering a column - -In the same way, you can generate a migration to alter a column by using the -command line, if the migration name is of the form "AlterXXXOnYYY": - -```bash -bin/cake bake migration AlterPriceOnProducts name:float -``` - -will generate: - -```php -table('products'); - $table->changeColumn('name', 'float'); - $table->update(); - } -} -``` - -> [!WARNING] -> Changing the type of a column can result in data loss if the -> current and target column type are not compatible. For example converting -> a varchar to float. - -### Removing a column - -In the same way, you can generate a migration to remove a column by using the -command line, if the migration name is of the form "RemoveXXXFromYYY": - -```bash -bin/cake bake migration RemovePriceFromProducts price -``` - -creates the file: +## Upgrading from 4.x -```php -table('products'); - $table->removeColumn('price') - ->save(); - } -} -``` +## Creating Migrations -> [!NOTE] -> The removeColumn command is not reversible, so must be called in the -> up method. A corresponding addColumn call should be added to the -> down method. +Migration naming conventions, bake patterns, column syntax, and generated +examples are covered in [Creating Migrations](getting-started/creating-migrations). ## Generating migration snapshots from an existing database -If you have a pre-existing database and want to start using -migrations, or to version control the initial schema of your application's -database, you can run the `bake migration_snapshot` command: - -```bash -bin/cake bake migration_snapshot Initial -``` - -It will generate a migration file called **YYYYMMDDHHMMSS_Initial.php** -containing all the create statements for all tables in your database. - -By default, the snapshot will be created by connecting to the database defined -in the `default` connection configuration. If you need to bake a snapshot from -a different datasource, you can use the `--connection` option: - -```bash -bin/cake bake migration_snapshot Initial --connection my_other_connection -``` - -You can also make sure the snapshot includes only the tables for which you have -defined the corresponding model classes by using the `--require-table` flag: - -```bash -bin/cake bake migration_snapshot Initial --require-table -``` - -When using the `--require-table` flag, the shell will look through your -application `Table` classes and will only add the model tables in the snapshot. - -If you want to generate a snapshot without marking it as migrated (for example, -for use in unit tests), you can use the `--generate-only` flag: - -```bash -bin/cake bake migration_snapshot Initial --generate-only -``` - -This will create the migration file but will not add an entry to the migrations -tracking table, allowing you to move the file to a different location without causing -"MISSING" status issues. - -The same logic will be applied implicitly if you wish to bake a snapshot for a -plugin. To do so, you need to use the `--plugin` option: - -```bash -bin/cake bake migration_snapshot Initial --plugin MyPlugin -``` - -Only the tables which have a `Table` object model class defined will be added -to the snapshot of your plugin. - -> [!NOTE] -> When baking a snapshot for a plugin, the migration files will be created -> in your plugin's **config/Migrations** directory. - -Be aware that when you bake a snapshot, it is automatically added to the -migrations log table as migrated. +For snapshot generation, diff workflows, and dump files, see +[Snapshots and Diffs](getting-started/snapshots-and-diffs). ## Generating a diff -As migrations are applied and rolled back, the migrations plugin will generate -a 'dump' file of your schema. If you make manual changes to your database schema -outside of migrations, you can use `bake migration_diff` to generate -a migration file that captures the difference between the current schema dump -file and database schema. To do so, you can use the following command: - -```bash -bin/cake bake migration_diff NameOfTheMigrations -``` - -By default, the diff will be created by connecting to the database defined -in the `default` connection configuration. -If you need to bake a diff from a different datasource, you can use the -`--connection` option: - -```bash -bin/cake bake migration_diff NameOfTheMigrations --connection my_other_connection -``` - -If you want to use the diff feature on an application that already has a -migrations history, you need to manually create the dump file that will be used -as comparison: - -```bash -bin/cake migrations dump -``` - -The database state must be the same as it would be if you just migrated all -your migrations before you create a dump file. -Once the dump file is generated, you can start doing changes in your database -and use the `bake migration_diff` command whenever you see fit. - -> [!NOTE] -> Migration diff generation can not detect column renamings. +The `bake migration_diff` workflow is also covered in +[Snapshots and Diffs](getting-started/snapshots-and-diffs). ## Applying Migrations -Once you have generated or written your migration file, you need to execute the -following command to apply the changes to your database: - -```bash -# Run all the migrations -bin/cake migrations migrate - -# Migrate to a specific version using the ``--target`` option -# or ``-t`` for short. -# The value is the timestamp that is prefixed to the migrations file name:: -bin/cake migrations migrate -t 20150103081132 - -# By default, migration files are looked for in the **config/Migrations** -# directory. You can specify the directory using the ``--source`` option -# or ``-s`` for short. -# The following example will run migrations in the **config/Alternate** -# directory -bin/cake migrations migrate -s Alternate - -# You can run migrations to a different connection than the ``default`` one -# using the ``--connection`` option or ``-c`` for short -bin/cake migrations migrate -c my_custom_connection - -# Migrations can also be run for plugins. Simply use the ``--plugin`` option -# or ``-p`` for short -bin/cake migrations migrate -p MyAwesomePlugin -``` +For `migrate`, `rollback`, `status`, `mark_migrated`, and related command +options, see +[Running and Managing Migrations](getting-started/running-and-managing-migrations). ## Reverting Migrations -The rollback command is used to undo previous migrations executed by this -plugin. It is the reverse action of the `migrate` command: - -```bash -# You can rollback to the previous migration by using the -# ``rollback`` command:: -bin/cake migrations rollback - -# You can also pass a migration version number to rollback -# to a specific version:: -bin/cake migrations rollback -t 20150103081132 -``` - -You can also use the `--source`, `--connection` and `--plugin` options -just like for the `migrate` command. +Rollback usage is documented in +[Running and Managing Migrations](getting-started/running-and-managing-migrations). ## View Migrations Status -The Status command prints a list of all migrations, along with their current -status. You can use this command to determine which migrations have been run: - -```bash -bin/cake migrations status -``` - -You can also output the results as a JSON formatted string using the -`--format` option (or `-f` for short): - -```bash -bin/cake migrations status --format json -``` - -You can also use the `--source`, `--connection` and `--plugin` options -just like for the `migrate` command. - -### Cleaning up missing migrations - -Sometimes migration files may be deleted from the filesystem but still exist -in the migrations tracking table. These migrations will be marked as **MISSING** in the -status output. You can remove these entries from the tracking table using the -`--cleanup` option: - -```bash -bin/cake migrations status --cleanup -``` - -This will remove all migration entries from the tracking table that no longer -have corresponding migration files in the filesystem. +Status commands, cleanup, and JSON output are documented in +[Running and Managing Migrations](getting-started/running-and-managing-migrations). ## Marking a migration as migrated -It can sometimes be useful to mark a set of migrations as migrated without -actually running them. In order to do this, you can use the `mark_migrated` -command. The command works seamlessly as the other commands. - -You can mark all migrations as migrated using this command: - -```bash -bin/cake migrations mark_migrated -``` - -You can also mark all migrations up to a specific version as migrated using -the `--target` option: - -```bash -bin/cake migrations mark_migrated --target=20151016204000 -``` - -If you do not want the targeted migration to be marked as migrated during the -process, you can use the `--exclude` flag with it: - -```bash -bin/cake migrations mark_migrated --target=20151016204000 --exclude -``` - -Finally, if you wish to mark only the targeted migration as migrated, you can -use the `--only` flag: - -```bash -bin/cake migrations mark_migrated --target=20151016204000 --only -``` - -You can also use the `--source`, `--connection` and `--plugin` options -just like for the `migrate` command. - -> [!NOTE] -> When you bake a snapshot with the `cake bake migration_snapshot` -> command, the created migration will automatically be marked as migrated. -> To prevent this behavior (e.g., for unit test migrations), use the -> `--generate-only` flag. - -This command expects the migration version number as argument: - -```bash -bin/cake migrations mark_migrated 20150420082532 -``` - -If you wish to mark all migrations as migrated, you can use the `all` special -value. If you use it, it will mark all found migrations as migrated: - -```bash -bin/cake migrations mark_migrated all -``` +See [Running and Managing Migrations](getting-started/running-and-managing-migrations) +for `mark_migrated` examples and caveats. ## Seeding your database -Seed classes are a good way to populate your database with default or starter -data. They are also a great way to generate data for development environments. - -By default, seeds will be looked for in the `config/Seeds/` directory of -your application. See the [Database Seeding](seeding) for how to build and use seed classes. +Seed classes are documented in [Database Seeding](guides/seeding). ## Generating a dump file -The dump command creates a file to be used with the `bake migration_diff` -command: - -```bash -bin/cake migrations dump -``` - -Each generated dump file is specific to the Connection it is generated from (and -is suffixed as such). This allows the `bake migration_diff` command to -properly compute diff in case your application is dealing with multiple database -possibly from different database vendors. - -Dump files are created in the same directory as your migrations files. - -You can also use the `--source`, `--connection` and `--plugin` options -just like for the `migrate` command. +Dump generation is documented in +[Snapshots and Diffs](getting-started/snapshots-and-diffs). ## Using Migrations for Tests -If you are using migrations for your application schema you can also use those -same migrations to build schema in your tests. In your application's -`tests/bootstrap.php` file you can use the `Migrator` class to build schema -when tests are run. The `Migrator` will use existing schema if it is current, -and if the migration history that is in the database differs from what is in the -filesystem, all tables will be dropped and migrations will be rerun from the -beginning: - -```php -// in tests/bootstrap.php -use Migrations\TestSuite\Migrator; - -$migrator = new Migrator(); - -// Simple setup for with no plugins -$migrator->run(); - -// Run a non 'test' database -$migrator->run(['connection' => 'test_other']); - -// Run migrations for plugins -$migrator->run(['plugin' => 'Contacts']); - -// Run the Documents migrations on the test_docs connection. -$migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']); -``` - -If you need to run multiple sets of migrations, those can be run as follows: - -```php -// Run migrations for plugin Contacts on the ``test`` connection, and Documents on the ``test_docs`` connection -$migrator->runMany([ - ['plugin' => 'Contacts'], - ['plugin' => 'Documents', 'connection' => 'test_docs'] -]); -``` - -If your database also contains tables that are not managed by your application -like those created by PostGIS, then you can exclude those tables from the drop -& truncate behavior using the `skip` option: - -```php -$migrator->run(['connection' => 'test', 'skip' => ['postgis*']]); -``` - -The `skip` option accepts a `fnmatch()` compatible pattern to exclude tables -from drop & truncate operations. - -If you need to see additional debugging output from migrations are being run, -you can enable a `debug` level logger. +Test bootstrapping with `Migrations\TestSuite\Migrator` is covered in +[Integration and Deployment](advanced/integration-and-deployment). ## Using Migrations In Plugins -Plugins can also provide migration files. This makes plugins that are intended -to be distributed much more portable and easy to install. All commands in the -Migrations plugin support the `--plugin` or `-p` option that will scope the -execution to the migrations relative to that plugin: - -```bash -bin/cake migrations status -p PluginName - -bin/cake migrations migrate -p PluginName -``` +Plugin-scoped migration workflows are covered in +[Integration and Deployment](advanced/integration-and-deployment). ## Running Migrations in a non-shell environment -While typical usage of migrations is from the command line, you can also run -migrations from a non-shell environment, by using -`Migrations\Migrations` class. This can be handy in case you are developing a plugin -installer for a CMS for instance. The `Migrations` class allows you to run the -following commands from the migrations shell: - -- migrate -- rollback -- markMigrated -- status -- seed - -Each of these commands has a method defined in the `Migrations` class. - -Here is how to use it: - -```php -use Migrations\Migrations; - -$migrations = new Migrations(); - -// Will return an array of all migrations and their status -$status = $migrations->status(); - -// Will return true if success. If an error occurred, an exception will be thrown -$migrate = $migrations->migrate(); - -// Will return true if success. If an error occurred, an exception will be thrown -$rollback = $migrations->rollback(); - -// Will return true if success. If an error occurred, an exception will be thrown -$markMigrated = $migrations->markMigrated(20150804222900); - -// Will return true if success. If an error occurred, an exception will be thrown -$seeded = $migrations->seed(); -``` - -The methods can accept an array of parameters that should match options from -the commands: - -```php -use Migrations\Migrations; - -$migrations = new Migrations(); - -// Will return an array of all migrations and their status -$status = $migrations->status(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); -``` - -You can pass any options the shell commands would take. -The only exception is the `markMigrated` command which is expecting the -version number of the migrations to mark as migrated as first argument. Pass -the array of parameters as the second argument for this method. - -Optionally, you can pass these parameters in the constructor of the class. -They will be used as default and this will prevent you from having to pass -them on each method call: - -```php -use Migrations\Migrations; - -$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); - -// All the following calls will be done with the parameters passed to the Migrations class constructor -$status = $migrations->status(); -$migrate = $migrations->migrate(); -``` - -If you need to override one or more default parameters for one call, you can -pass them to the method: - -```php -use Migrations\Migrations; - -$migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); - -// This call will be made with the "custom" connection -$status = $migrations->status(); -// This one with the "default" connection -$migrate = $migrations->migrate(['connection' => 'default']); -``` - - +Programmatic execution through `Migrations\Migrations` is covered in +[Integration and Deployment](advanced/integration-and-deployment). ## Feature Flags -Migrations offers a few feature flags for compatibility. These features are disabled by default but can be enabled if required: - -- `unsigned_primary_keys`: Should Migrations create primary keys as unsigned integers? (default: `false`) -- `unsigned_ints`: Should Migrations create all integer columns as unsigned? (default: `false`) -- `column_null_default`: Should Migrations create columns as null by default? (default: `false`) -- `add_timestamps_use_datetime`: Should Migrations use `DATETIME` type - columns for the columns added by `addTimestamps()`. - -Set them via Configure to enable (e.g. in `config/app.php`): - -```text -'Migrations' => [ - 'unsigned_primary_keys' => true, - 'unsigned_ints' => true, - 'column_null_default' => true, -], -``` - -> [!NOTE] -> The `unsigned_primary_keys` and `unsigned_ints` options only affect MySQL databases. -> When generating migrations with `bake migration_snapshot` or `bake migration_diff`, -> the `signed` attribute will only be included in the output for unsigned columns -> (as `'signed' => false`). Signed is the default for integer columns in MySQL, so -> `'signed' => true` is never output. +Compatibility feature flags are covered in +[Integration and Deployment](advanced/integration-and-deployment). ## Skipping the `schema.lock` file generation -In order for the diff feature to work, a **.lock** file is generated everytime -you migrate, rollback or bake a snapshot, to keep track of the state of your -database schema at any given point in time. You can skip this file generation, -for instance when deploying on your production environment, by using the -`--no-lock` option for the aforementioned command: - -```bash -bin/cake migrations migrate --no-lock - -bin/cake migrations rollback --no-lock - -bin/cake bake migration_snapshot MyMigration --no-lock -``` +The `--no-lock` workflow is covered in +[Integration and Deployment](advanced/integration-and-deployment). ## Deployment -You should update your deployment scripts to run migrations when new code is -deployed. Ideally you want to run migrations after the code is on your servers, -but before the application code becomes active. - -After running migrations remember to clear the ORM cache so it renews the column -metadata of your tables. Otherwise, you might end up having errors about -columns not existing when performing operations on those new columns. The -CakePHP Core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you -can use to perform this operation: - -```bash -bin/cake migration migrate -bin/cake schema_cache clear -``` +Deployment guidance and schema cache refresh steps are covered in +[Integration and Deployment](advanced/integration-and-deployment). ## Alert of missing migrations -You can use the `Migrations.PendingMigrations` middleware in local development -to alert developers about new migrations that have not been applied: - -```php -use Migrations\Middleware\PendingMigrationsMiddleware; - -$config = [ - 'plugins' => [ - ... // Optionally include a list of plugins with migrations to check. - ], -]; - -$middlewareQueue - ... // ErrorHandler middleware - ->add(new PendingMigrationsMiddleware($config)) - ... // rest -``` - -You can add `'app'` config key set to `false` if you are only interested in -checking plugin migrations. - -You can temporarily disable the migration check by adding -`skip-migration-check=1` to the URL query string +Local development alerts for pending migrations are covered in +[Integration and Deployment](advanced/integration-and-deployment). ## IDE autocomplete support -The [IdeHelper plugin](https://github.com/dereuromark/cakephp-ide-helper) can help -you to get more IDE support for the tables, their column names and possible column types. -Specifically PHPStorm understands the meta information and can help you autocomplete those. +IDE-related tooling notes are covered in +[Integration and Deployment](advanced/integration-and-deployment). diff --git a/docs/en/index.rst b/docs/en/index.rst deleted file mode 100644 index f180413e9..000000000 --- a/docs/en/index.rst +++ /dev/null @@ -1,955 +0,0 @@ -Migrations 5.x -############## - -Migrations is a plugin that lets you track changes to your database schema over -time as PHP code that accompanies your application. This lets you ensure each -environment your application runs in has the appropriate schema by applying -migrations. - -Instead of writing schema modifications in SQL, this plugin allows you to -define schema changes with a high-level database portable API. - -What's New in 5.x -================= - -Migrations 5.x includes several new features: - -- **Seed tracking** - Seeds are now tracked in a ``cake_seeds`` table, preventing - accidental re-runs -- **Check constraints** - Support for database check constraints via ``addCheckConstraint()`` -- **Default values in bake** - Specify default values when baking migrations - (e.g., ``active:boolean:default[true]``) -- **MySQL ALTER options** - Control ``ALGORITHM`` and ``LOCK`` for ALTER TABLE operations -- **insertOrSkip()** - New method for idempotent seed data insertion - -See the :doc:`upgrading` guide for breaking changes and migration steps from 4.x. - -Installation -============ - -By default Migrations is installed with the application skeleton. If -you've removed it and want to re-install it, you can do so by running the -following from your application's ROOT directory (where **composer.json** file is -located): - -.. code-block:: bash - - php composer.phar require cakephp/migrations "@stable" - - # Or if composer is installed globally - composer require cakephp/migrations "@stable" - -To use the plugin you'll need to load it in your application's -**config/bootstrap.php** file. You can use `CakePHP's Plugin shell -`__ to -load and unload plugins from your **config/bootstrap.php**: - -.. code-block:: bash - - bin/cake plugin load Migrations - -Or you can load the plugin by editing your **src/Application.php** file and -adding the following statement:: - - $this->addPlugin('Migrations'); - -Additionally, you will need to configure the default database configuration for -your application in your **config/app.php** file as explained in the `Database -Configuration section -`__. - -Upgrading from 4.x -================== - -If you are upgrading from Migrations 4.x, please see the :doc:`upgrading` guide -for breaking changes and migration steps. - -Overview -======== - -A migration is a PHP file that describes the changes to apply to your database. -A migration file can add, change or remove tables, columns, indexes and foreign keys. - -If we wanted to create a table, we could use a migration similar to this:: - - table('products'); - $table->addColumn('name', 'string', [ - 'default' => null, - 'limit' => 255, - 'null' => false, - ]); - $table->addColumn('description', 'text', [ - 'default' => null, - 'null' => false, - ]); - $table->addColumn('created', 'datetime', [ - 'default' => null, - 'null' => false, - ]); - $table->addColumn('modified', 'datetime', [ - 'default' => null, - 'null' => false, - ]); - $table->create(); - } - } - -When applied, this migration will add a table to your database named -``products`` with the following column definitions: - -- ``id`` column of type ``integer`` as primary key. This column is added - implicitly, but you can customize the name and type if necessary. -- ``name`` column of type ``string`` -- ``description`` column of type ``text`` -- ``created`` column of type ``datetime`` -- ``modified`` column of type ``datetime`` - -.. note:: - - Migrations are not automatically applied, you can apply and rollback - migrations with CLI commands. - -Once the file has been created in the **config/Migrations** folder, you can -apply it: - -.. code-block:: bash - - bin/cake migrations migrate - -Creating Migrations -=================== - -Migration files are stored in the **config/Migrations** directory of your -application. The name of the migration files are prefixed with the date in -which they were created, in the format **YYYYMMDDHHMMSS_MigrationName.php**. -Here are examples of migration filenames: - -* **20160121163850_CreateProducts.php** -* **20160210133047_AddRatingToProducts.php** - -The easiest way to create a migrations file is by using ``bin/cake bake -migration`` CLI command: - -.. code-block:: bash - - bin/cake bake migration CreateProducts - -This will create an empty migration that you can edit to add any columns, -indexes and foreign keys you need. See the :ref:`creating-a-table` section to -learn more about using migrations to define tables. - -.. note:: - - Migrations need to be applied using ``bin/cake migrations migrate`` after - they have been created. - -Migration file names --------------------- - -When generating a migration, you can follow one of the following patterns -to have additional skeleton code generated: - -* ``/^(Create)(.*)/`` Creates the specified table. -* ``/^(Drop)(.*)/`` Drops the specified table. - Ignores specified field arguments -* ``/^(Add).*(?:To)(.*)/`` Adds fields to the specified - table -* ``/^(Remove).*(?:From)(.*)/`` Removes fields from the - specified table -* ``/^(Alter)(.*)/`` Alters the specified table. An alias - for CreateTable and AddField. -* ``/^(Alter).*(?:On)(.*)/`` Alters fields from the specified table. - -You can also use the ``underscore_form`` as the name for your migrations i.e. -``create_products``. - -.. warning:: - - Migration names are used as class names, and thus may collide with - other migrations if the class names are not unique. In this case, it may be - necessary to manually override the name at a later date, or simply change - the name you are specifying. - -Creating a table ----------------- - -You can use ``bake migration`` to create a table: - -.. code-block:: bash - - bin/cake bake migration CreateProducts name:string description:text created modified - -The command line above will generate a migration file that resembles:: - - table('products'); - $table->addColumn('name', 'string', [ - 'default' => null, - 'limit' => 255, - 'null' => false, - ]); - $table->addColumn('description', 'text', [ - 'default' => null, - 'null' => false, - ]); - $table->addColumn('created', 'datetime', [ - 'default' => null, - 'null' => false, - ]); - $table->addColumn('modified', 'datetime', [ - 'default' => null, - 'null' => false, - ]); - $table->create(); - } - } - -Column syntax -------------- - -The ``bake migration`` command provides a compact syntax to define columns when -generating a migration: - -.. code-block:: bash - - bin/cake bake migration CreateProducts name:string description:text created modified - -You can use the column syntax when creating tables and adding columns. You can -also edit the migration after generation to add or customize the columns - -Columns on the command line follow the following pattern:: - - fieldName:fieldType?[length]:default[value]:indexType:indexName - -For instance, the following are all valid ways of specifying an email field: - -* ``email:string?`` -* ``email:string:unique`` -* ``email:string?[50]`` -* ``email:string:unique:EMAIL_INDEX`` -* ``email:string[120]:unique:EMAIL_INDEX`` - -While defining decimal columns, the ``length`` can be defined to have precision -and scale, separated by a comma. - -* ``amount:decimal[5,2]`` -* ``amount:decimal?[5,2]`` - -Columns with a question mark after the fieldType will make the column nullable. - -The ``length`` part is optional and should always be written between bracket. - -The ``default[value]`` part is optional and sets the default value for the column. -Supported value types include: - -* Booleans: ``true`` or ``false`` - e.g., ``active:boolean:default[true]`` -* Integers: ``0``, ``123``, ``-456`` - e.g., ``count:integer:default[0]`` -* Floats: ``1.5``, ``-2.75`` - e.g., ``rate:decimal:default[1.5]`` -* Strings: ``'hello'`` or ``"world"`` (quoted) - e.g., ``status:string:default['pending']`` -* Null: ``null`` or ``NULL`` - e.g., ``description:text?:default[null]`` -* SQL expressions: ``CURRENT_TIMESTAMP`` - e.g., ``created_at:datetime:default[CURRENT_TIMESTAMP]`` - -Fields named ``created`` and ``modified``, as well as any field with a ``_at`` -suffix, will automatically be set to the type ``datetime``. - -There are some heuristics to choosing fieldtypes when left unspecified or set to -an invalid value. Default field type is ``string``: - -* id: integer -* created, modified, updated: datetime -* latitude, longitude (or short forms lat, lng): decimal - -Additionally you can create an empty migrations file if you want full control -over what needs to be executed, by omitting to specify a columns definition: - -.. code-block:: bash - - bin/cake migrations create MyCustomMigration - - -See :doc:`writing-migrations` for more information on how to use ``Table`` -objects to interact with tables and define schema changes. - -Adding columns to an existing table ------------------------------------ - -If the migration name in the command line is of the form "AddXXXToYYY" and is -followed by a list of column names and types then a migration file containing -the code for creating the columns will be generated: - -.. code-block:: bash - - bin/cake bake migration AddPriceToProducts price:decimal[5,2] - -Executing the command line above will generate:: - - table('products'); - $table->addColumn('price', 'decimal', [ - 'default' => null, - 'null' => false, - 'precision' => 5, - 'scale' => 2, - ]); - $table->update(); - } - } - -Adding a column with an index ------------------------------ - -It is also possible to add indexes to columns: - -.. code-block:: bash - - bin/cake bake migration AddNameIndexToProducts name:string:index - -will generate:: - - table('products'); - $table->addColumn('name', 'string') - ->addColumn('email', 'string') - ->addIndex(['name']) - // add a unique index: - ->addIndex('email', ['unique' => true]) - ->update(); - } - } - -Adding a column with a default value -------------------------------------- - -You can specify default values for columns using the ``default[value]`` syntax: - -.. code-block:: bash - - bin/cake bake migration AddActiveToUsers active:boolean:default[true] - -will generate:: - - table('users'); - $table->addColumn('active', 'boolean', [ - 'default' => true, - 'null' => false, - ]); - $table->update(); - } - } - -You can combine default values with other options like nullable and indexes: - -.. code-block:: bash - - bin/cake bake migration AddStatusToOrders status:string:default['pending']:unique - -Altering a column ------------------ - -In the same way, you can generate a migration to alter a column by using the -command line, if the migration name is of the form "AlterXXXOnYYY": - -.. code-block:: bash - - bin/cake bake migration AlterPriceOnProducts name:float - -will generate:: - - table('products'); - $table->changeColumn('name', 'float'); - $table->update(); - } - } - -.. warning:: - - Changing the type of a column can result in data loss if the - current and target column type are not compatible. For example converting - a varchar to float. - -Removing a column ------------------ - -In the same way, you can generate a migration to remove a column by using the -command line, if the migration name is of the form "RemoveXXXFromYYY": - -.. code-block:: bash - - bin/cake bake migration RemovePriceFromProducts price - -creates the file:: - - table('products'); - $table->removeColumn('price') - ->save(); - } - } - -.. note:: - - The `removeColumn` command is not reversible, so must be called in the - `up` method. A corresponding `addColumn` call should be added to the - `down` method. - -Generating migration snapshots from an existing database -======================================================== - -If you have a pre-existing database and want to start using -migrations, or to version control the initial schema of your application's -database, you can run the ``bake migration_snapshot`` command: - -.. code-block:: bash - - bin/cake bake migration_snapshot Initial - -It will generate a migration file called **YYYYMMDDHHMMSS_Initial.php** -containing all the create statements for all tables in your database. - -By default, the snapshot will be created by connecting to the database defined -in the ``default`` connection configuration. If you need to bake a snapshot from -a different datasource, you can use the ``--connection`` option: - -.. code-block:: bash - - bin/cake bake migration_snapshot Initial --connection my_other_connection - -You can also make sure the snapshot includes only the tables for which you have -defined the corresponding model classes by using the ``--require-table`` flag: - -.. code-block:: bash - - bin/cake bake migration_snapshot Initial --require-table - -When using the ``--require-table`` flag, the shell will look through your -application ``Table`` classes and will only add the model tables in the snapshot. - -If you want to generate a snapshot without marking it as migrated (for example, -for use in unit tests), you can use the ``--generate-only`` flag: - -.. code-block:: bash - - bin/cake bake migration_snapshot Initial --generate-only - -This will create the migration file but will not add an entry to the migrations -tracking table, allowing you to move the file to a different location without causing -"MISSING" status issues. - -The same logic will be applied implicitly if you wish to bake a snapshot for a -plugin. To do so, you need to use the ``--plugin`` option: - -.. code-block:: bash - - bin/cake bake migration_snapshot Initial --plugin MyPlugin - -Only the tables which have a ``Table`` object model class defined will be added -to the snapshot of your plugin. - -.. note:: - - When baking a snapshot for a plugin, the migration files will be created - in your plugin's **config/Migrations** directory. - -Be aware that when you bake a snapshot, it is automatically added to the -migrations log table as migrated. - -Generating a diff -================= - -As migrations are applied and rolled back, the migrations plugin will generate -a 'dump' file of your schema. If you make manual changes to your database schema -outside of migrations, you can use ``bake migration_diff`` to generate -a migration file that captures the difference between the current schema dump -file and database schema. To do so, you can use the following command: - -.. code-block:: bash - - bin/cake bake migration_diff NameOfTheMigrations - -By default, the diff will be created by connecting to the database defined -in the ``default`` connection configuration. -If you need to bake a diff from a different datasource, you can use the -``--connection`` option: - -.. code-block:: bash - - bin/cake bake migration_diff NameOfTheMigrations --connection my_other_connection - -If you want to use the diff feature on an application that already has a -migrations history, you need to manually create the dump file that will be used -as comparison: - -.. code-block:: bash - - bin/cake migrations dump - -The database state must be the same as it would be if you just migrated all -your migrations before you create a dump file. -Once the dump file is generated, you can start doing changes in your database -and use the ``bake migration_diff`` command whenever you see fit. - -.. note:: - - Migration diff generation can not detect column renamings. - -Applying Migrations -=================== - -Once you have generated or written your migration file, you need to execute the -following command to apply the changes to your database: - -.. code-block:: bash - - # Run all the migrations - bin/cake migrations migrate - - # Migrate to a specific version using the ``--target`` option - # or ``-t`` for short. - # The value is the timestamp that is prefixed to the migrations file name:: - bin/cake migrations migrate -t 20150103081132 - - # By default, migration files are looked for in the **config/Migrations** - # directory. You can specify the directory using the ``--source`` option - # or ``-s`` for short. - # The following example will run migrations in the **config/Alternate** - # directory - bin/cake migrations migrate -s Alternate - - # You can run migrations to a different connection than the ``default`` one - # using the ``--connection`` option or ``-c`` for short - bin/cake migrations migrate -c my_custom_connection - - # Migrations can also be run for plugins. Simply use the ``--plugin`` option - # or ``-p`` for short - bin/cake migrations migrate -p MyAwesomePlugin - -Reverting Migrations -==================== - -The rollback command is used to undo previous migrations executed by this -plugin. It is the reverse action of the ``migrate`` command: - -.. code-block:: bash - - # You can rollback to the previous migration by using the - # ``rollback`` command:: - bin/cake migrations rollback - - # You can also pass a migration version number to rollback - # to a specific version:: - bin/cake migrations rollback -t 20150103081132 - -You can also use the ``--source``, ``--connection`` and ``--plugin`` options -just like for the ``migrate`` command. - -View Migrations Status -====================== - -The Status command prints a list of all migrations, along with their current -status. You can use this command to determine which migrations have been run: - -.. code-block:: bash - - bin/cake migrations status - -You can also output the results as a JSON formatted string using the -``--format`` option (or ``-f`` for short): - -.. code-block:: bash - - bin/cake migrations status --format json - -You can also use the ``--source``, ``--connection`` and ``--plugin`` options -just like for the ``migrate`` command. - -Cleaning up missing migrations -------------------------------- - -Sometimes migration files may be deleted from the filesystem but still exist -in the migrations tracking table. These migrations will be marked as **MISSING** in the -status output. You can remove these entries from the tracking table using the -``--cleanup`` option: - -.. code-block:: bash - - bin/cake migrations status --cleanup - -This will remove all migration entries from the tracking table that no longer -have corresponding migration files in the filesystem. - -Marking a migration as migrated -=============================== - -It can sometimes be useful to mark a set of migrations as migrated without -actually running them. In order to do this, you can use the ``mark_migrated`` -command. The command works seamlessly as the other commands. - -You can mark all migrations as migrated using this command: - -.. code-block:: bash - - bin/cake migrations mark_migrated - -You can also mark all migrations up to a specific version as migrated using -the ``--target`` option: - -.. code-block:: bash - - bin/cake migrations mark_migrated --target=20151016204000 - -If you do not want the targeted migration to be marked as migrated during the -process, you can use the ``--exclude`` flag with it: - -.. code-block:: bash - - bin/cake migrations mark_migrated --target=20151016204000 --exclude - -Finally, if you wish to mark only the targeted migration as migrated, you can -use the ``--only`` flag: - -.. code-block:: bash - - bin/cake migrations mark_migrated --target=20151016204000 --only - -You can also use the ``--source``, ``--connection`` and ``--plugin`` options -just like for the ``migrate`` command. - -.. note:: - - When you bake a snapshot with the ``cake bake migration_snapshot`` - command, the created migration will automatically be marked as migrated. - To prevent this behavior (e.g., for unit test migrations), use the - ``--generate-only`` flag. - -This command expects the migration version number as argument: - -.. code-block:: bash - - bin/cake migrations mark_migrated 20150420082532 - -If you wish to mark all migrations as migrated, you can use the ``all`` special -value. If you use it, it will mark all found migrations as migrated: - -.. code-block:: bash - - bin/cake migrations mark_migrated all - -Seeding your database -===================== - -Seed classes are a good way to populate your database with default or starter -data. They are also a great way to generate data for development environments. - -By default, seeds will be looked for in the ``config/Seeds/`` directory of -your application. See the :doc:`seeding` for how to build and use seed classes. - -Generating a dump file -====================== - -The dump command creates a file to be used with the ``bake migration_diff`` -command: - -.. code-block:: bash - - bin/cake migrations dump - -Each generated dump file is specific to the Connection it is generated from (and -is suffixed as such). This allows the ``bake migration_diff`` command to -properly compute diff in case your application is dealing with multiple database -possibly from different database vendors. - -Dump files are created in the same directory as your migrations files. - -You can also use the ``--source``, ``--connection`` and ``--plugin`` options -just like for the ``migrate`` command. - - -Using Migrations for Tests -========================== - -If you are using migrations for your application schema you can also use those -same migrations to build schema in your tests. In your application's -``tests/bootstrap.php`` file you can use the ``Migrator`` class to build schema -when tests are run. The ``Migrator`` will use existing schema if it is current, -and if the migration history that is in the database differs from what is in the -filesystem, all tables will be dropped and migrations will be rerun from the -beginning:: - - // in tests/bootstrap.php - use Migrations\TestSuite\Migrator; - - $migrator = new Migrator(); - - // Simple setup for with no plugins - $migrator->run(); - - // Run a non 'test' database - $migrator->run(['connection' => 'test_other']); - - // Run migrations for plugins - $migrator->run(['plugin' => 'Contacts']); - - // Run the Documents migrations on the test_docs connection. - $migrator->run(['plugin' => 'Documents', 'connection' => 'test_docs']); - - -If you need to run multiple sets of migrations, those can be run as follows:: - - // Run migrations for plugin Contacts on the ``test`` connection, and Documents on the ``test_docs`` connection - $migrator->runMany([ - ['plugin' => 'Contacts'], - ['plugin' => 'Documents', 'connection' => 'test_docs'] - ]); - -If your database also contains tables that are not managed by your application -like those created by PostGIS, then you can exclude those tables from the drop -& truncate behavior using the ``skip`` option:: - - $migrator->run(['connection' => 'test', 'skip' => ['postgis*']]); - -The ``skip`` option accepts a ``fnmatch()`` compatible pattern to exclude tables -from drop & truncate operations. - -If you need to see additional debugging output from migrations are being run, -you can enable a ``debug`` level logger. - -Using Migrations In Plugins -=========================== - -Plugins can also provide migration files. This makes plugins that are intended -to be distributed much more portable and easy to install. All commands in the -Migrations plugin support the ``--plugin`` or ``-p`` option that will scope the -execution to the migrations relative to that plugin: - -.. code-block:: bash - - bin/cake migrations status -p PluginName - - bin/cake migrations migrate -p PluginName - -Running Migrations in a non-shell environment -============================================= - -While typical usage of migrations is from the command line, you can also run -migrations from a non-shell environment, by using -``Migrations\Migrations`` class. This can be handy in case you are developing a plugin -installer for a CMS for instance. The ``Migrations`` class allows you to run the -following commands from the migrations shell: - -* migrate -* rollback -* markMigrated -* status -* seed - -Each of these commands has a method defined in the ``Migrations`` class. - -Here is how to use it:: - - use Migrations\Migrations; - - $migrations = new Migrations(); - - // Will return an array of all migrations and their status - $status = $migrations->status(); - - // Will return true if success. If an error occurred, an exception will be thrown - $migrate = $migrations->migrate(); - - // Will return true if success. If an error occurred, an exception will be thrown - $rollback = $migrations->rollback(); - - // Will return true if success. If an error occurred, an exception will be thrown - $markMigrated = $migrations->markMigrated(20150804222900); - - // Will return true if success. If an error occurred, an exception will be thrown - $seeded = $migrations->seed(); - -The methods can accept an array of parameters that should match options from -the commands:: - - use Migrations\Migrations; - - $migrations = new Migrations(); - - // Will return an array of all migrations and their status - $status = $migrations->status(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); - -You can pass any options the shell commands would take. -The only exception is the ``markMigrated`` command which is expecting the -version number of the migrations to mark as migrated as first argument. Pass -the array of parameters as the second argument for this method. - -Optionally, you can pass these parameters in the constructor of the class. -They will be used as default and this will prevent you from having to pass -them on each method call:: - - use Migrations\Migrations; - - $migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); - - // All the following calls will be done with the parameters passed to the Migrations class constructor - $status = $migrations->status(); - $migrate = $migrations->migrate(); - -If you need to override one or more default parameters for one call, you can -pass them to the method:: - - use Migrations\Migrations; - - $migrations = new Migrations(['connection' => 'custom', 'source' => 'MyMigrationsFolder']); - - // This call will be made with the "custom" connection - $status = $migrations->status(); - // This one with the "default" connection - $migrate = $migrations->migrate(['connection' => 'default']); - -.. _feature-flags: - -Feature Flags -============= - -Migrations offers a few feature flags for compatibility. These features are disabled by default but can be enabled if required: - -* ``unsigned_primary_keys``: Should Migrations create primary keys as unsigned integers? (default: ``false``) -* ``unsigned_ints``: Should Migrations create all integer columns as unsigned? (default: ``false``) -* ``column_null_default``: Should Migrations create columns as null by default? (default: ``false``) -* ``add_timestamps_use_datetime``: Should Migrations use ``DATETIME`` type - columns for the columns added by ``addTimestamps()``. - -Set them via Configure to enable (e.g. in ``config/app.php``):: - - 'Migrations' => [ - 'unsigned_primary_keys' => true, - 'unsigned_ints' => true, - 'column_null_default' => true, - ], - -.. note:: - - The ``unsigned_primary_keys`` and ``unsigned_ints`` options only affect MySQL databases. - When generating migrations with ``bake migration_snapshot`` or ``bake migration_diff``, - the ``signed`` attribute will only be included in the output for unsigned columns - (as ``'signed' => false``). Signed is the default for integer columns in MySQL, so - ``'signed' => true`` is never output. - -Skipping the ``schema.lock`` file generation -============================================ - -In order for the diff feature to work, a **.lock** file is generated everytime -you migrate, rollback or bake a snapshot, to keep track of the state of your -database schema at any given point in time. You can skip this file generation, -for instance when deploying on your production environment, by using the -``--no-lock`` option for the aforementioned command: - -.. code-block:: bash - - bin/cake migrations migrate --no-lock - - bin/cake migrations rollback --no-lock - - bin/cake bake migration_snapshot MyMigration --no-lock - -Deployment -========== - -You should update your deployment scripts to run migrations when new code is -deployed. Ideally you want to run migrations after the code is on your servers, -but before the application code becomes active. - -After running migrations remember to clear the ORM cache so it renews the column -metadata of your tables. Otherwise, you might end up having errors about -columns not existing when performing operations on those new columns. The -CakePHP Core includes a `Schema Cache Shell -`__ that you -can use to perform this operation: - -.. code-block:: bash - - bin/cake migration migrate - bin/cake schema_cache clear - -Alert of missing migrations -=========================== - -You can use the ``Migrations.PendingMigrations`` middleware in local development -to alert developers about new migrations that have not been applied:: - - use Migrations\Middleware\PendingMigrationsMiddleware; - - $config = [ - 'plugins' => [ - ... // Optionally include a list of plugins with migrations to check. - ], - ]; - - $middlewareQueue - ... // ErrorHandler middleware - ->add(new PendingMigrationsMiddleware($config)) - ... // rest - -You can add ``'app'`` config key set to ``false`` if you are only interested in -checking plugin migrations. - -You can temporarily disable the migration check by adding -``skip-migration-check=1`` to the URL query string - -IDE autocomplete support -======================== - -The `IdeHelper plugin -`__ can help -you to get more IDE support for the tables, their column names and possible column types. -Specifically PHPStorm understands the meta information and can help you autocomplete those. diff --git a/docs/en/seeding.rst b/docs/en/seeding.rst deleted file mode 100644 index d2aedf34b..000000000 --- a/docs/en/seeding.rst +++ /dev/null @@ -1,616 +0,0 @@ -Database Seeding -################ - -Seed classes are a great way to easily fill your database with data after -it's created. By default, they are stored in the ``config/Seeds`` directory. - -.. note:: - - Database seeding is entirely optional, and Migrations does not create a Seeds - directory by default. - -Creating a New Seed Class -========================= - -Migrations includes a command to easily generate a new seed class: - -.. code-block:: bash - - $ bin/cake bake seed MyNewSeed - -By default, it generates a traditional seed class with a named class: - -.. code-block:: php - - call('Articles');`` or ``$this->call('ArticlesSeed');`` - -Using the short name is recommended for cleaner, more concise code. - -Anonymous Seed Classes ----------------------- - -Migrations also supports generating anonymous seed classes, which use PHP's -anonymous class feature instead of named classes. This style is useful for: - -- Avoiding namespace declarations -- Better PHPCS compatibility (no class name to filename matching required) -- Simpler file structure without named class constraints - -To generate an anonymous seed class, use the ``--style anonymous`` option: - -.. code-block:: bash - - $ bin/cake bake seed MyNewSeed --style anonymous - -This generates a seed file using an anonymous class: - -.. code-block:: php - - [ - 'style' => 'anonymous', // or 'traditional' - ], - -Seed Options ------------- - -.. code-block:: bash - # You specify the name of the table the seed files will alter by using the ``--table`` option - bin/cake bake seed Articles --table my_articles_table - - # You can specify a plugin to bake into - bin/cake bake seed Articles --plugin PluginName - - # You can specify an alternative connection when generating a seed. - bin/cake bake seed Articles --connection connection - - # Include data from the Articles table in your seed. - bin/cake bake seed --data Articles - -By default, it will export all the rows found in your table. You can limit the -number of rows exported by using the ``--limit`` option: - -.. code-block:: bash - - # Will only export the first 10 rows found - bin/cake bake seed --data --limit 10 Articles - -If you only want to include a selection of fields from the table in your seed -file, you can use the ``--fields`` option. It takes the list of fields to -include as a comma separated value string: - -.. code-block:: bash - - # Will only export the fields `id`, `title` and `excerpt` - bin/cake bake seed --data --fields id,title,excerpt Articles - -.. tip:: - - Of course you can use both the ``--limit`` and ``--fields`` options in the - same command call. - -.. _custom-seed-migration-templates: - -Customizing Seed and Migration templates ----------------------------------------- - -Because migrations uses `bake `__ under the hood -you can customize the templates that migrations uses for creating seeds and -migrations by creating templates in your application. Custom templates for -migrations should be on one of the following paths: - -- ``ROOT/templates/plugin/Migrations/bake/`` -- ``ROOT/templates/bake/`` - -For example, the seed templates are: - -- Traditional: ``Seed/seed.twig`` at **ROOT/templates/plugin/Migrations/bake/Seed/seed.twig** -- Anonymous: ``Seed/seed-anonymous.twig`` at **ROOT/templates/plugin/Migrations/bake/Seed/seed-anonymous.twig** - -The BaseSeed Class -================== - -All Migrations seeds extend from the ``BaseSeed`` class. -It provides the necessary support to create your seed classes. Seed -classes are primarily used to insert test data. - -The Run Method -============== - -The run method is automatically invoked by Migrations when you execute the -``cake seeds run`` command. You should use this method to insert your test -data. - -Seed Execution Tracking -======================== - -Seeds track their execution state in the ``cake_seeds`` database table. By default, -a seed will only run once. If you attempt to run a seed that has already been -executed, it will be skipped with an "already executed" message. - -To re-run a seed that has already been executed, use the ``--force`` flag: - -.. code-block:: bash - - bin/cake seeds run Users --force - -You can check which seeds have been executed using the status command: - -.. code-block:: bash - - bin/cake seeds status - -To reset all seeds' execution state (allowing them to run again without ``--force``): - -.. code-block:: bash - - bin/cake seeds reset - -.. note:: - - When re-running seeds with ``--force``, be careful to ensure your seeds are - idempotent (safe to run multiple times) or they may create duplicate data. - -Customizing the Seed Tracking Table ------------------------------------- - -By default, seed execution is tracked in a table named ``cake_seeds``. You can -customize this table name by configuring it in your ``config/app.php`` or -``config/app_local.php``: - -.. code-block:: php - - 'Migrations' => [ - 'seed_table' => 'my_custom_seeds_table', - ], - -This is useful if you need to avoid table name conflicts or want to follow -a specific naming convention in your database. - -Idempotent Seeds -================ - -Some seeds are designed to be run multiple times safely (idempotent), such as seeds -that update configuration or reference data. For these seeds, you can override the -``isIdempotent()`` method: - -.. code-block:: php - - execute(" - INSERT INTO settings (setting_key, setting_value) - VALUES ('app_version', '2.0.0') - ON DUPLICATE KEY UPDATE setting_value = '2.0.0' - "); - - // Or check before inserting - $exists = $this->fetchRow( - "SELECT COUNT(*) as count FROM settings WHERE setting_key = 'maintenance_mode'" - ); - - if ($exists['count'] === 0) { - $this->table('settings')->insert([ - 'setting_key' => 'maintenance_mode', - 'setting_value' => 'false', - ])->save(); - } - } - } - -When ``isIdempotent()`` returns ``true``: - -- The seed will run **every time** you execute ``seeds run`` -- The last execution time is still tracked in the ``cake_seeds`` table -- The ``seeds status`` command will show the seed as ``(idempotent)`` -- You must ensure the seed's ``run()`` method handles duplicate executions safely - -This is useful for: - -- Configuration seeds that should always reflect current values -- Reference data that may need periodic updates -- Seeds that use ``INSERT ... ON DUPLICATE KEY UPDATE`` or similar patterns -- Development/testing seeds that need to run repeatedly - -.. warning:: - - Only mark a seed as idempotent if you've verified it's safe to run multiple times. - Otherwise, you may create duplicate data or other unexpected behavior. - -The Init Method -=============== - -The ``init()`` method is run by Migrations before the run method if it exists. This -can be used to initialize properties of the Seed class before using run. - -The Should Execute Method -========================= - -The ``shouldExecute()`` method is run by Migrations before executing the seed. -This can be used to prevent the seed from being executed at this time. It always -returns true by default. You can override it in your custom ``BaseSeed`` -implementation. - -Foreign Key Dependencies -======================== - -Often you'll find that seeds need to run in a particular order, so they don't -violate foreign key constraints. To define this order, you can implement the -``getDependencies()`` method that returns an array of seeds to run before the -current seed: - -.. code-block:: php - - call('Another'); // Short name without 'Seed' suffix - $this->call('YetAnother'); // Short name without 'Seed' suffix - - // You can use the plugin dot syntax to call seeds from a plugin - $this->call('PluginName.FromPlugin'); - } - } - -You can also use the full seed name including the ``Seed`` suffix: - -.. code-block:: php - - $this->call('AnotherSeed'); - $this->call('YetAnotherSeed'); - $this->call('PluginName.FromPluginSeed'); - -Both forms are supported and work identically. - -Inserting Data -============== - -Seed classes can also use the familiar ``Table`` object to insert data. You can -retrieve an instance of the Table object by calling the ``table()`` method from -within your seed class and then use the ``insert()`` method to insert data: - -.. code-block:: php - - 'foo', - 'created' => date('Y-m-d H:i:s'), - ],[ - 'body' => 'bar', - 'created' => date('Y-m-d H:i:s'), - ] - ]; - - $posts = $this->table('posts'); - $posts->insert($data) - ->saveData(); - } - } - -.. note:: - - You must call the ``saveData()`` method to commit your data to the table. - Migrations will buffer data until you do so. - -Insert Modes -============ - -In addition to the standard ``insert()`` method, Migrations provides specialized -insert methods for handling conflicts with existing data. - -Insert or Skip --------------- - -The ``insertOrSkip()`` method inserts rows but silently skips any that would -violate a unique constraint: - -.. code-block:: php - - 'USD', 'name' => 'US Dollar'], - ['code' => 'EUR', 'name' => 'Euro'], - ]; - - $this->table('currencies') - ->insertOrSkip($data) - ->saveData(); - } - } - -Insert or Update (Upsert) -------------------------- - -The ``insertOrUpdate()`` method performs an "upsert" operation - inserting new -rows and updating existing rows that conflict on unique columns: - -.. code-block:: php - - 'USD', 'rate' => 1.0000], - ['code' => 'EUR', 'rate' => 0.9234], - ]; - - $this->table('exchange_rates') - ->insertOrUpdate($data, ['rate'], ['code']) - ->saveData(); - } - } - -The method takes three arguments: - -- ``$data``: The rows to insert (same format as ``insert()``) -- ``$updateColumns``: Which columns to update when a conflict occurs -- ``$conflictColumns``: Which columns define uniqueness (must have a unique index) - -.. warning:: - - Database-specific behavior differences: - - **MySQL**: Uses ``ON DUPLICATE KEY UPDATE``. The ``$conflictColumns`` parameter - is ignored because MySQL automatically applies the update to *all* unique - constraint violations on the table. Passing ``$conflictColumns`` will trigger - a warning. If your table has multiple unique constraints, be aware that a - conflict on *any* of them will trigger the update. - - **PostgreSQL/SQLite**: Uses ``ON CONFLICT (...) DO UPDATE SET``. The - ``$conflictColumns`` parameter is required and specifies exactly which unique - constraint should trigger the update. A ``RuntimeException`` will be thrown - if this parameter is empty. - - **SQL Server**: Not currently supported. Use separate insert/update logic. - -Truncating Tables -================= - -In addition to inserting data Migrations makes it trivial to empty your tables using the -SQL `TRUNCATE` command: - -.. code-block:: php - - 'foo', - 'created' => date('Y-m-d H:i:s'), - ], - [ - 'body' => 'bar', - 'created' => date('Y-m-d H:i:s'), - ] - ]; - - $posts = $this->table('posts'); - $posts->insert($data) - ->saveData(); - - // empty the table - $posts->truncate(); - } - } - -.. note:: - - SQLite doesn't natively support the ``TRUNCATE`` command so behind the scenes - ``DELETE FROM`` is used. It is recommended to call the ``VACUUM`` command - after truncating a table. Migrations does not do this automatically. - -Executing Seed Classes -====================== - -This is the easy part. To seed your database, simply use the ``seeds run`` command: - -.. code-block:: bash - - $ bin/cake seeds run - -By default, Migrations will execute all available seed classes. If you would like to -run a specific seed, simply pass in the seed name as an argument. -You can use either the short name (without the ``Seed`` suffix) or the full name: - -.. code-block:: bash - - $ bin/cake seeds run User - # or - $ bin/cake seeds run UserSeed - -Both commands work identically. - -You can also run multiple seeds by separating them with commas: - -.. code-block:: bash - - $ bin/cake seeds run User,Permission,Log - # or with full names - $ bin/cake seeds run UserSeed,PermissionSeed,LogSeed - -You can also use the `-v` parameter for more output verbosity: - -.. code-block:: bash - - $ bin/cake seeds run -v - -The Migrations seed functionality provides a simple mechanism to easily and repeatably -insert test data into your database, this is great for development environment -sample data or getting state for demos. diff --git a/docs/en/upgrading.md b/docs/en/upgrades/upgrading-from-4-x.md similarity index 97% rename from docs/en/upgrading.md rename to docs/en/upgrades/upgrading-from-4-x.md index c34d6c36a..0786baa8b 100644 --- a/docs/en/upgrading.md +++ b/docs/en/upgrades/upgrading-from-4-x.md @@ -103,12 +103,12 @@ $row = $stmt->fetch('assoc'); Seeds are now tracked in a `cake_seeds` table by default, preventing accidental re-runs. Use `--force` to run a seed again, or `bin/cake seeds reset` to clear tracking. -See [Database Seeding](seeding) for more details. +See [Database Seeding](../guides/seeding) for more details. ### Check Constraints Support for database check constraints via `addCheckConstraint()`. -See [Writing Migrations](writing-migrations) for usage details. +See [Writing Migrations](../guides/writing-migrations) for usage details. ### MySQL ALTER Options diff --git a/docs/en/upgrading-to-builtin-backend.md b/docs/en/upgrades/upgrading-to-builtin-backend.md similarity index 100% rename from docs/en/upgrading-to-builtin-backend.md rename to docs/en/upgrades/upgrading-to-builtin-backend.md diff --git a/docs/en/upgrading.rst b/docs/en/upgrading.rst deleted file mode 100644 index d413bff46..000000000 --- a/docs/en/upgrading.rst +++ /dev/null @@ -1,174 +0,0 @@ -Upgrading from 4.x to 5.x -######################### - -Migrations 5.x includes significant changes from 4.x. This guide outlines -the breaking changes and what you need to update when upgrading. - -Requirements -============ - -- **PHP 8.2+** is now required (was PHP 8.1+) -- **CakePHP 5.3+** is now required -- **Phinx has been removed** - The builtin backend is now the only supported backend - -If you were already using the builtin backend in 4.x (introduced in 4.3, default in 4.4), -the upgrade should be straightforward. See :doc:`upgrading-to-builtin-backend` for more -details on API differences between the phinx and builtin backends. - -Command Changes -=============== - -The phinx wrapper commands have been removed. The new command structure is: - -Migrations ----------- - -The migration commands remain unchanged: - -.. code-block:: bash - - bin/cake migrations migrate - bin/cake migrations rollback - bin/cake migrations status - bin/cake migrations mark_migrated - bin/cake migrations dump - -Seeds ------ - -Seed commands have changed: - -.. code-block:: bash - - # 4.x # 5.x - bin/cake migrations seed bin/cake seeds run - bin/cake migrations seed --seed X bin/cake seeds run X - -The new seed commands are: - -- ``bin/cake seeds run`` - Run seed classes -- ``bin/cake seeds run SeedName`` - Run a specific seed -- ``bin/cake seeds status`` - Show seed execution status -- ``bin/cake seeds reset`` - Reset seed execution tracking - -Maintaining Backward Compatibility ----------------------------------- - -If you need to maintain the old ``migrations seed`` command for existing scripts or -CI/CD pipelines, you can add command aliases in your ``src/Application.php``:: - - public function console(CommandCollection $commands): CommandCollection - { - $commands = $this->addConsoleCommands($commands); - - // Add backward compatibility alias - $commands->add('migrations seed', \Migrations\Command\SeedCommand::class); - - return $commands; - } - -Removed Classes and Namespaces -============================== - -The following have been removed in 5.x: - -- ``Migrations\Command\Phinx\*`` - All phinx wrapper commands -- ``Migrations\Command\MigrationsCommand`` - Use ``bin/cake migrations`` entry point -- ``Migrations\Command\MigrationsSeedCommand`` - Use ``bin/cake seeds run`` -- ``Migrations\Command\MigrationsCacheBuildCommand`` - Schema cache is managed differently -- ``Migrations\Command\MigrationsCacheClearCommand`` - Schema cache is managed differently -- ``Migrations\Command\MigrationsCreateCommand`` - Use ``bin/cake bake migration`` - -If you have code that directly references any of these classes, you will need to update it. - -API Changes -=========== - -Adapter Query Results ---------------------- - -If your migrations use ``AdapterInterface::query()`` to fetch rows, the return type has -changed from a phinx result to ``Cake\Database\StatementInterface``:: - - // 4.x (phinx) - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetchAll(); - $row = $stmt->fetch(); - - // 5.x (builtin) - $stmt = $this->getAdapter()->query('SELECT * FROM articles'); - $rows = $stmt->fetchAll('assoc'); - $row = $stmt->fetch('assoc'); - -New Features in 5.x -=================== - -5.x includes several new features: - -Seed Tracking -------------- - -Seeds are now tracked in a ``cake_seeds`` table by default, preventing accidental re-runs. -Use ``--force`` to run a seed again, or ``bin/cake seeds reset`` to clear tracking. -See :doc:`seeding` for more details. - -Check Constraints ------------------ - -Support for database check constraints via ``addCheckConstraint()``. -See :doc:`writing-migrations` for usage details. - -MySQL ALTER Options -------------------- - -Support for ``ALGORITHM`` and ``LOCK`` options on MySQL ALTER TABLE operations, -allowing control over how MySQL performs schema changes. - -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 -============================ - -Your existing migration files should work without changes in most cases. The builtin backend -provides the same API as phinx for common operations. - -If you encounter issues with existing migrations, please report them at -https://github.com/cakephp/migrations/issues diff --git a/docs/en/writing-migrations.md b/docs/en/writing-migrations.md deleted file mode 100644 index fd2fd88bb..000000000 --- a/docs/en/writing-migrations.md +++ /dev/null @@ -1,2527 +0,0 @@ -# Writing Migrations - -Migrations are a declarative API that helps you transform your database. Each migration -is represented by a PHP class in a unique file. It is preferred that you write -your migrations using the Migrations API, but raw SQL is also supported. - -## Creating a New Migration - -Let's start by creating a new migration with `bake`: - -```bash -bin/cake bake migration -``` - -This will create a new migration in the format -`YYYYMMDDHHMMSS_my_new_migration.php`, where the first 14 characters are -replaced with the current timestamp down to the second. - -If you have specified multiple migration paths, you will be asked to select -which path to create the new migration in. - -Bake will automatically creates a skeleton migration file with a single method: - -```php - [ - 'style' => 'anonymous', // or 'traditional' -], -``` - -This configuration also applies to seeds, allowing you to use consistent styling -across your entire project. - -## The Change Method - -Migrations supports 'reversible migrations'. In many scenarios, you -only need to define the `up` logic, and Migrations can figure out how to -generate the rollback operations for you. For example: - -```php -table('user_logins'); - $table->addColumn('user_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - } -} -``` - -When executing this migration, Migrations will create the `user_logins` table on -the way up and automatically figure out how to drop the table on the way down. -Please be aware that when a `change` method exists, Migrations will -ignore the `up` and `down` methods. If you need to use these methods it is -recommended to create a separate migration file. - -> [!NOTE] -> When creating or updating tables inside a `change()` method you must use -> the Table `create()` and `update()` methods. Migrations cannot automatically -> determine whether a `save()` call is creating a new table or modifying an -> existing one. - -The following actions are reversible when done through the Table API in -Migrations, and will be automatically reversed: - -- Creating a table -- Renaming a table -- Adding a column -- Renaming a column -- Adding an index -- Adding a foreign key -- Adding a check constraint - -If a command cannot be reversed then Migrations will throw an -`IrreversibleMigrationException` when it's migrating down. If you wish to -use a command that cannot be reversed in the change function, you can use an -if statement with `$this->isMigratingUp()` to only run things in the -up or down direction. For example: - -```php -table('user_logins'); - $table->addColumn('user_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - if ($this->isMigratingUp()) { - $table->insert([['user_id' => 1, 'created' => '2020-01-19 03:14:07']]) - ->save(); - } - } -} -``` - -## The Up Method - -The up method is automatically run by Migrations when you are migrating up and it -detects the given migration hasn't been executed previously. You should use the -up method to transform the database with your intended changes. - -## The Down Method - -The down method is automatically run by Migrations when you are migrating down and -it detects the given migration has been executed in the past. You should use -the down method to reverse/undo the transformations described in the up method. - -## The Init Method - -The `init()` method is run by Migrations before the migration methods if it exists. -This can be used for setting common class properties that are then used within -the migration methods. - -## The Should Execute Method - -The `shouldExecute()` method is run by Migrations before executing the migration. -This can be used to prevent the migration from being executed at this time. It always -returns true by default. You can override it in your custom `BaseMigration` -implementation. - -## Working With Tables - -The Table object enables you to easily manipulate database tables using PHP -code. You can retrieve an instance of the Table object by calling the -`table()` method from within your database migration: - -```php -table('tableName'); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -You can then manipulate this table using the methods provided by the Table -object. - - - -## Adding Columns - -Column types are specified as strings and can be one of: - -- binary -- boolean -- char -- date -- datetime -- decimal -- float -- double -- smallinteger -- integer -- biginteger -- string -- text -- time -- timestamp -- uuid -- binaryuuid -- nativeuuid - -In addition, the MySQL adapter supports `enum`, `set`, `blob`, -`tinyblob`, `mediumblob`, `longblob`, `bit` and `json` column types -(`json` in MySQL 5.7 and above). When providing a limit value and using -`binary`, `varbinary` or `blob` and its subtypes, the retained column type -will be based on required length (see [Limit Option and MySQL](#limit-option-and-mysql) for details). - -With most adapters, the `uuid` and `nativeuuid` column types are aliases, -however with the MySQL adapter + MariaDB, the `nativeuuid` type maps to -a native uuid column instead of `CHAR(36)` like `uuid` does. - -In addition, the Postgres adapter supports `interval`, `json`, `jsonb`, -`uuid`, `cidr`, `inet` and `macaddr` column types (PostgreSQL 9.3 and -above). - -### Valid Column Options - -The following are valid column options: - -For any column type: - -| Option | Description | -|----|----| -| limit | set maximum length for strings, also hints column types in adapters (see note below) | -| length | alias for `limit` | -| default | set default value or action | -| null | allow `NULL` values, defaults to `true` (setting `identity` will override default to `false`) | -| after | specify the column that a new column should be placed after, or use `\Migrations\Db\Adapter\MysqlAdapter::FIRST` to place the column at the start of the table *(only applies to MySQL)* | -| comment | set a text comment on the column | - -For `decimal` and `float` columns: - -| Option | Description | -|----|----| -| precision | total number of digits (e.g., 10 in `DECIMAL(10,2)`) | -| scale | number of digits after the decimal point (e.g., 2 in `DECIMAL(10,2)`) | -| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | - -> [!NOTE] -> **Precision and Scale Terminology** -> -> Migrations follows the SQL standard where `precision` represents the total number of digits, -> and `scale` represents digits after the decimal point. For example, to create `DECIMAL(10,2)` -> (10 total digits with 2 decimal places): -> -> ``` php -> $table->addColumn('price', 'decimal', [ -> 'precision' => 10, // Total digits -> 'scale' => 2, // Decimal places -> ]); -> ``` -> -> This differs from CakePHP's TableSchema which uses `length` for total digits and -> `precision` for decimal places. The migration adapter handles this conversion automatically. - -For `enum` and `set` columns: - -| Option | Description | -|--------|-----------------------------------------------------| -| values | Can be a comma separated list or an array of values | - -For `smallinteger`, `integer` and `biginteger` columns: - -| Option | Description | -|----|----| -| identity | enable or disable automatic incrementing (if enabled, will set `null: false` if `null` option is not set) | -| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | - -For Postgres, when using `identity`, it will utilize the `serial` type -appropriate for the integer size, so that `smallinteger` will give you -`smallserial`, `integer` gives `serial`, and `biginteger` gives -`bigserial`. - -For `date` columns: - -| Option | Description | -|---------|---------------------------------------------| -| default | set default value (use with `CURRENT_DATE`) | - -For `time` columns: - -| Option | Description | -|----|----| -| default | set default value (use with `CURRENT_TIME`) | -| timezone | enable or disable the `with time zone` option *(only applies to Postgres)* | - -For `datetime` columns: - -| Option | Description | -|----|----| -| default | set default value (use with `CURRENT_TIMESTAMP`) | -| timezone | enable or disable the `with time zone` option *(only applies to Postgres)* | - -For `timestamp` columns: - -| Option | Description | -|----|----| -| default | set default value (use with `CURRENT_TIMESTAMP`) | -| update | set an action to be triggered when the row is updated (use with `CURRENT_TIMESTAMP`) *(only applies to MySQL)* | -| timezone | enable or disable the `with time zone` option for `time` and `timestamp` columns *(only applies to Postgres)* | - -You can add `created` and `updated` timestamps to a table using the -`addTimestamps()` method. This method accepts three arguments, where the first -two allow setting alternative names for the columns while the third argument -allows you to enable the `timezone` option for the columns. The defaults for -these arguments are `created`, `updated`, and `false` respectively. For -the first and second argument, if you provide `null`, then the default name -will be used, and if you provide `false`, then that column will not be -created. Please note that attempting to set both to `false` will throw -a `\RuntimeException`. Additionally, you can use the -`addTimestampsWithTimezone()` method, which is an alias to `addTimestamps()` -that will always set the third argument to `true` (see examples below). The -`created` column will have a default set to `CURRENT_TIMESTAMP`. For MySQL -only, `updated` column will have update set to -`CURRENT_TIMESTAMP`: - -```php -table('users')->addTimestamps()->create(); - // Use defaults (with timezones) - $table = $this->table('users')->addTimestampsWithTimezone()->create(); - - // Override the 'created' column name with 'recorded_at'. - $table = $this->table('books')->addTimestamps('recorded_at')->create(); - - // Override the 'updated' column name with 'amended_at', preserving timezones. - // The two lines below do the same, the second one is simply cleaner. - $table = $this->table('books')->addTimestamps(null, 'amended_at', true)->create(); - $table = $this->table('users')->addTimestampsWithTimezone(null, 'amended_at')->create(); - - // Only add the created column to the table - $table = $this->table('books')->addTimestamps(null, false); - // Only add the updated column to the table - $table = $this->table('users')->addTimestamps(false); - // Note, setting both false will throw a \RuntimeError - } -} -``` - -For `boolean` columns: - -| Option | Description | -|--------|-------------------------------------------------------------------| -| signed | enable or disable the `unsigned` option *(only applies to MySQL)* | - -For `string` and `text` columns: - -| Option | Description | -|----|----| -| collation | set collation that differs from table defaults *(only applies to MySQL)* | -| encoding | set character set that differs from table defaults *(only applies to MySQL)* | - -### Limit Option and MySQL - -When using the MySQL adapter, there are a couple things to consider when working with limits: - -- When using a `string` primary key or index on MySQL 5.7 or below, or the - MyISAM storage engine, and the default charset of `utf8mb4_unicode_ci`, you - must specify a limit less than or equal to 191, or use a different charset. -- Additional hinting of database column type can be made for `integer`, - `text`, `blob`, `tinyblob`, `mediumblob`, `longblob` columns. Using - `limit` with one the following options will modify the column type - accordingly: - -| Limit | Column Type | -|--------------|-------------| -| BLOB_TINY | TINYBLOB | -| BLOB_REGULAR | BLOB | -| BLOB_MEDIUM | MEDIUMBLOB | -| BLOB_LONG | LONGBLOB | -| TEXT_TINY | TINYTEXT | -| TEXT_REGULAR | TEXT | -| TEXT_MEDIUM | MEDIUMTEXT | -| TEXT_LONG | LONGTEXT | -| INT_TINY | TINYINT | -| INT_SMALL | SMALLINT | -| INT_MEDIUM | MEDIUMINT | -| INT_REGULAR | INT | -| INT_BIG | BIGINT | - -For `binary` or `varbinary` types, if limit is set greater than allowed 255 -bytes, the type will be changed to the best matching blob type given the -length: - -```php -table('cart_items'); -$table->addColumn('user_id', 'integer') - ->addColumn('product_id', 'integer', ['limit' => MysqlAdapter::INT_BIG]) - ->addColumn('subtype_id', 'integer', ['limit' => MysqlAdapter::INT_SMALL]) - ->addColumn('quantity', 'integer', ['limit' => MysqlAdapter::INT_TINY]) - ->create(); -``` - -### Default values with expressions - -If you need to set a default to an expression, you can use a `Literal` to have -the column's default value used without any quoting or escaping. This is helpful -when you want to use a function as a default value: - -```php -use Migrations\BaseMigration; -use Migrations\Db\Literal; - -class AddSomeColumns extends BaseMigration -{ - public function change(): void - { - $this->table('users') - ->addColumn('uniqid', 'uuid', [ - 'default' => Literal::from('uuid_generate_v4()') - ]) - ->create(); - } -} -``` - - - -### Creating a Table - -Creating a table is really easy using the Table object. Let's create a table to -store a collection of users: - -```php -table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->create(); - } -} -``` - -Columns are added using the `addColumn()` method. We create a unique index -for both the username and email columns using the `addIndex()` method. -Finally calling `create()` commits the changes to the database. - -> [!NOTE] -> Migrations automatically creates an auto-incrementing primary key column called `id` for every -> table. - -The `id` option sets the name of the automatically created identity field, -while the `primary_key` option selects the field or fields used for primary -key. `id` will always override the `primary_key` option unless it's set to -false. If you don't need a primary key set `id` to false without specifying -a `primary_key`, and no primary key will be created. - -To specify an alternate primary key, you can specify the `primary_key` option -when accessing the Table object. Let's disable the automatic `id` column and -create a primary key using two columns instead: - -```php -table('followers', ['id' => false, 'primary_key' => ['user_id', 'follower_id']]); - $table->addColumn('user_id', 'integer') - ->addColumn('follower_id', 'integer') - ->addColumn('created', 'datetime') - ->create(); - } -} -``` - -Setting a single `primary_key` doesn't enable the `AUTO_INCREMENT` option. -To simply change the name of the primary key, we need to override the default `id` field name: - -```php -table('followers', ['id' => 'user_id']); - $table->addColumn('follower_id', 'integer') - ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) - ->create(); - } -} -``` - -In addition, the MySQL adapter supports following options: - -| Option | Platform | Description | -|----|----|----| -| comment | MySQL, Postgres | set a text comment on the table | -| collation | MySQL, SqlServer | set the table collation *(defaults to database collation)* | -| row_format | MySQL | set the table row format | -| engine | MySQL | define table engine *(defaults to \`\`InnoDB\`\`)* | -| signed | MySQL | whether the primary key is `signed` *(defaults to \`\`true\`\`)* | -| limit | MySQL | set the maximum length for the primary key | - -By default, the primary key is `signed`. -To set it to be unsigned, pass the `signed` option with a `false` -value, or enable the `Migrations.unsigned_primary_keys` and -`Migrations.unsigned_ints` feature flags (see [Feature Flags](index#feature-flags)). -Both flags should be used together so that foreign key columns match -the primary keys they reference: - -```php -table('followers', ['signed' => false]); - $table->addColumn('follower_id', 'integer') - ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) - ->create(); - } -} -``` - -If you need to create a table with a different collation than the database, -use: - -```php -table('categories', [ - 'collation' => 'latin1_german1_ci' - ]) - ->addColumn('title', 'string') - ->create(); - } -} -``` - -Note however this can only be done on table creation : there is currently no way -of adding a column to an existing table with a different collation than the -table or the database. Only `MySQL` and `SqlServer` supports this -configuration key for the time being. - -To view available column types and options, see [Adding Columns](#adding-columns) for details. - -### MySQL ALTER TABLE Options - -::: info Added in version 5.0.0 -`ALGORITHM` and `LOCK` options were added in 5.0.0. -::: - -When modifying tables in MySQL, you can control how the ALTER TABLE operation is -performed using the `algorithm` and `lock` options. This is useful for performing -zero-downtime schema changes on large tables in production environments. - -```php -table('large_table'); - $table->addIndex(['status'], [ - 'name' => 'idx_status', - ]); - $table->update([ - 'algorithm' => 'INPLACE', - 'lock' => 'NONE', - ]); - } -} -``` - -Available `algorithm` values: - -| Algorithm | Description | -|----|----| -| DEFAULT | Let MySQL choose the algorithm (default behavior) | -| INPLACE | Modify the table in place without copying data (when possible) | -| COPY | Create a copy of the table with the changes (legacy method) | -| INSTANT | Only modify metadata, no table rebuild (MySQL 8.0+, limited operations) | - -Available `lock` values: - -| Lock | Description | -|-----------|----------------------------------------------------------| -| DEFAULT | Use minimal locking for the algorithm (default behavior) | -| NONE | Allow concurrent reads and writes during the operation | -| SHARED | Allow concurrent reads but block writes | -| EXCLUSIVE | Block all reads and writes during the operation | - -> [!NOTE] -> Not all operations support all algorithm/lock combinations. MySQL will raise -> an error if the requested combination is not possible for the operation. -> The `INSTANT` algorithm is only available in MySQL 8.0+ and only for specific -> operations like adding columns at the end of a table. - -> [!WARNING] -> Using `ALGORITHM=INPLACE, LOCK=NONE` does not guarantee zero-downtime for -> all operations. Some operations may still require a table copy or exclusive lock. -> Always test schema changes on a staging environment first. - -### Table Partitioning - -Migrations supports table partitioning for MySQL and PostgreSQL. Partitioning helps -manage large tables by splitting them into smaller, more manageable pieces. - -> [!NOTE] -> Partition columns must be included in the primary key for MySQL. SQLite does -> not support partitioning. MySQL's `RANGE` and `LIST` types only work with -> integer columns - use `RANGE COLUMNS` and `LIST COLUMNS` for DATE/STRING columns. - -#### RANGE Partitioning - -RANGE partitioning is useful when you want to partition by numeric ranges. For MySQL, -use `TYPE_RANGE` with integer columns or expressions, and `TYPE_RANGE_COLUMNS` for -DATE/DATETIME/STRING columns: - -```php -table('orders', [ - 'id' => false, - 'primary_key' => ['id', 'order_date'], - ]); - $table->addColumn('id', 'integer', ['identity' => true]) - ->addColumn('order_date', 'date') - ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2]) - ->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date') - ->addPartition('p2022', '2023-01-01') - ->addPartition('p2023', '2024-01-01') - ->addPartition('p2024', '2025-01-01') - ->addPartition('pmax', 'MAXVALUE') - ->create(); - } -} -``` - -#### LIST Partitioning - -LIST partitioning is useful when you want to partition by discrete values. For MySQL, -use `TYPE_LIST` with integer columns and `TYPE_LIST_COLUMNS` for STRING columns: - -```php -table('customers', [ - 'id' => false, - 'primary_key' => ['id', 'region'], - ]); - $table->addColumn('id', 'integer', ['identity' => true]) - ->addColumn('region', 'string', ['limit' => 20]) - ->addColumn('name', 'string') - ->partitionBy(Partition::TYPE_LIST_COLUMNS, 'region') - ->addPartition('p_americas', ['US', 'CA', 'MX', 'BR']) - ->addPartition('p_europe', ['UK', 'DE', 'FR', 'IT']) - ->addPartition('p_asia', ['JP', 'CN', 'IN', 'KR']) - ->create(); - } -} -``` - -#### HASH Partitioning - -HASH partitioning distributes data evenly across a specified number of partitions: - -```php -table('sessions'); - $table->addColumn('user_id', 'integer') - ->addColumn('data', 'text') - ->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8]) - ->create(); - } -} -``` - -#### KEY Partitioning (MySQL only) - -KEY partitioning is similar to HASH but uses MySQL's internal hashing function: - -```php -table('cache', [ - 'id' => false, - 'primary_key' => ['cache_key'], - ]); - $table->addColumn('cache_key', 'string', ['limit' => 255]) - ->addColumn('value', 'binary') - ->partitionBy(Partition::TYPE_KEY, 'cache_key', ['count' => 16]) - ->create(); - } -} -``` - -#### Partitioning with Expressions - -You can partition by expressions using the `Literal` class: - -```php -table('events', [ - 'id' => false, - 'primary_key' => ['id', 'created_at'], - ]); - $table->addColumn('id', 'integer', ['identity' => true]) - ->addColumn('created_at', 'datetime') - ->partitionBy(Partition::TYPE_RANGE, Literal::from('YEAR(created_at)')) - ->addPartition('p2022', 2023) - ->addPartition('p2023', 2024) - ->addPartition('pmax', 'MAXVALUE') - ->create(); - } -} -``` - -#### Modifying Partitions on Existing Tables - -You can add or drop partitions on existing partitioned tables: - -```php -table('orders') - ->addPartitionToExisting('p2025', '2026-01-01') - ->update(); - } - - public function down(): void - { - // Drop the partition - $this->table('orders') - ->dropPartition('p2025') - ->update(); - } -} -``` - -### Saving Changes - -When working with the Table object, Migrations stores certain operations in a -pending changes cache. Once you have made the changes you want to the table, -you must save them. To perform this operation, Migrations provides three methods, -`create()`, `update()`, and `save()`. `create()` will first create -the table and then run the pending changes. `update()` will just run the -pending changes, and should be used when the table already exists. `save()` -is a helper function that checks first if the table exists and if it does not -will run `create()`, else it will run `update()`. - -As stated above, when using the `change()` migration method, you should always -use `create()` or `update()`, and never `save()` as otherwise migrating -and rolling back may result in different states, due to `save()` calling -`create()` when running migrate and then `update()` on rollback. When -using the `up()`/`down()` methods, it is safe to use either `save()` or -the more explicit methods. - -When in doubt with working with tables, it is always recommended to call -the appropriate function and commit any pending changes to the database. - -### Renaming a Column - -To rename a column, access an instance of the Table object then call the -`renameColumn()` method: - -```php -table('users'); - $table->renameColumn('bio', 'biography') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('users'); - $table->renameColumn('biography', 'bio') - ->save(); - } -} -``` - -### Adding a Column After Another Column - -When adding a column with the MySQL adapter, you can dictate its position using -the `after` option, where its value is the name of the column to position it -after: - -```php -table('users'); - $table->addColumn('city', 'string', ['after' => 'email']) - ->update(); - } -} -``` - -This would create the new column `city` and position it after the `email` -column. The `\Migrations\Db\Adapter\MysqlAdapter::FIRST` constant can be used -to specify that the new column should be created as the first column in that -table. - -### Dropping a Column - -To drop a column, use the `removeColumn()` method: - -```php -table('users'); - $table->removeColumn('short_name') - ->save(); - } -} -``` - -### Specifying a Column Limit - -You can limit the maximum length of a column by using the `limit` option: - -```php -table('tags'); - $table->addColumn('short_name', 'string', ['limit' => 30]) - ->update(); - } -} -``` - -### Changing Column Attributes - -There are two methods for modifying existing columns: - -#### Updating Columns (Recommended) - -To modify specific column attributes while preserving others, use the `updateColumn()` method. -This method automatically preserves unspecified attributes like defaults, nullability, limits, etc.: - -```php -table('users'); - // Make email nullable, preserving all other attributes - $users->updateColumn('email', null, ['null' => true]) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->updateColumn('email', null, ['null' => false]) - ->save(); - } -} -``` - -You can pass `null` as the column type to preserve the existing type, or specify a new type: - -```php -// Preserve type and other attributes, only change nullability -$table->updateColumn('email', null, ['null' => true]); - -// Change type to biginteger, preserve default and other attributes -$table->updateColumn('user_id', 'biginteger'); - -// Change default value, preserve everything else -$table->updateColumn('status', null, ['default' => 'active']); -``` - -The following attributes are automatically preserved by `updateColumn()`: - -- Default values -- NULL/NOT NULL constraint -- Column limit/length -- Decimal scale/precision -- Comments -- Signed/unsigned (for numeric types) -- Collation and encoding -- Enum/set values - -#### Changing Columns (Traditional) - -To completely replace a column definition, use the `changeColumn()` method. -This method requires you to specify all desired column attributes. -See [Adding Columns](#adding-columns) and [Valid Column Options](#valid-column-options) for allowed values: - -```php -table('users'); - // Must specify all attributes - $users->changeColumn('email', 'string', [ - 'limit' => 255, - 'null' => true, - 'default' => null, - ]) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -You can enable attribute preservation with `changeColumn()` by passing -`'preserveUnspecified' => true` in the options: - -```php -$table->changeColumn('email', 'string', [ - 'null' => true, - 'preserveUnspecified' => true, -]); -``` - -> [!NOTE] -> For most use cases, `updateColumn()` is recommended as it is safer and requires -> less code. Use `changeColumn()` when you need to completely redefine a column -> or when working with legacy code that expects the traditional behavior. - -### Working With Indexes - -To add an index to a table you can simply call the `addIndex()` method on the -table object: - -```php -table('users'); - $table->addColumn('city', 'string') - ->addIndex(['city']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -By default Migrations instructs the database adapter to create a simple index. We -can pass an additional parameter `unique` to the `addIndex()` method to -specify a unique index. We can also explicitly specify a name for the index -using the `name` parameter, the index columns sort order can also be specified using -the `order` parameter. The order parameter takes an array of column names and sort order key/value pairs: - -```php -table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addIndex(['email', 'username'], [ - 'unique' => true, - 'name' => 'idx_users_email', - 'order' => ['email' => 'DESC', 'username' => 'ASC']] - ) - ->save(); - } -} -``` - -As of 4.6.0, you can use `BaseMigration::index()` to get a fluent builder to -define indexes: - -```php -table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addIndex( - $this->index(['email', 'username']) - ->setType('unique') - ->setName('idx_users_email') - ->setOrder(['email' => 'DESC', 'username' => 'ASC']) - ) - ->save(); - } -} -``` - -The MySQL adapter also supports `fulltext` indexes. If you are using a version before 5.6 you must -ensure the table uses the `MyISAM` engine: - -```php -table('users', ['engine' => 'MyISAM']); - $table->addColumn('email', 'string') - ->addIndex('email', ['type' => 'fulltext']) - ->create(); - } -} -``` - -MySQL adapter supports setting the index length defined by limit option. -When you are using a multi-column index, you are able to define each column index length. -The single column index can define its index length with or without defining column name in limit option: - -```php -table('users'); - $table->addColumn('email', 'string') - ->addColumn('username','string') - ->addColumn('user_guid', 'string', ['limit' => 36]) - ->addIndex(['email','username'], ['limit' => ['email' => 5, 'username' => 2]]) - ->addIndex('user_guid', ['limit' => 6]) - ->create(); - } -} -``` - -The SQL Server and PostgreSQL adapters support `include` (non-key) columns on indexes: - -```php -table('users'); - $table->addColumn('email', 'string') - ->addColumn('firstname','string') - ->addColumn('lastname','string') - ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) - ->create(); - } -} -``` - -PostgreSQL, SQLServer, and SQLite support partial indexes by defining where -clauses for the index: - -```php -table('users'); - $table->addColumn('email', 'string') - ->addColumn('is_verified','boolean') - ->addIndex( - $this->index('email') - ->setName('user_email_verified_idx') - ->setType('unique') - ->setWhere('is_verified = true') - ) - ->create(); - } -} -``` - -PostgreSQL can create indexes concurrently which avoids taking disruptive locks -during index creation: - -```php -table('users'); - $table->addColumn('email', 'string') - ->addIndex( - $this->index('email') - ->setName('user_email_unique_idx') - ->setType('unique') - ->setConcurrently(true) - ) - ->create(); - } -} -``` - -PostgreSQL adapters also supports Generalized Inverted Index `gin` indexes: - -```php -table('users'); - $table->addColumn('address', 'string') - ->addIndex('address', ['type' => 'gin']) - ->create(); - } -} -``` - -Removing indexes is as easy as calling the `removeIndex()` method. You must -call this method for each index: - -```php -table('users'); - $table->removeIndex(['email']) - ->save(); - - // alternatively, you can delete an index by its name, ie: - $table->removeIndexByName('idx_users_email') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -::: info Added in version 4.6.0 -`Index::setWhere()`, and `Index::setConcurrently()` were added. -::: - -### Working With Foreign Keys - -Migrations has support for creating foreign key constraints on your database tables. -Let's add a foreign key to an example table: - -```php -table('tags'); - $table->addColumn('tag_name', 'string') - ->save(); - - $refTable = $this->table('tag_relationships'); - $refTable->addColumn('tag_id', 'integer', ['null' => true]) - ->addForeignKey( - 'tag_id', - 'tags', - 'id', - ['delete'=> 'SET_NULL', 'update'=> 'NO_ACTION'], - ) - ->save(); - - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -The 'delete' and 'update' options allow you to define the `ON UPDATE` and `ON DELETE` behavior. Possibles values are 'SET_NULL', 'NO_ACTION', 'CASCADE' and -'RESTRICT'. If 'SET_NULL' is used then the column must be created as nullable -with the option `['null' => true]`. - -Foreign keys can be defined with arrays of columns to build constraints between -tables with composite keys: - -```php -table('follower_events'); - $table->addColumn('user_id', 'integer') - ->addColumn('follower_id', 'integer') - ->addColumn('event_id', 'integer') - ->addForeignKey( - ['user_id', 'follower_id'], - 'followers', - ['user_id', 'follower_id'], - [ - 'delete'=> 'NO_ACTION', - 'update'=> 'NO_ACTION', - 'constraint' => 'user_follower_id', - ] - ) - ->save(); - } -} -``` - -The options parameter of `addForeignKey()` supports the following options: - -| Option | Description | -|------------|--------------------------------------------------------| -| update | set an action to be triggered when the row is updated | -| delete | set an action to be triggered when the row is deleted | -| constraint | set a name to be used by foreign key constraint | -| deferrable | define deferred constraint application (postgres only) | - -Using the `foreignKey()` method provides a fluent builder to define a foreign -key: - -```php -table('articles'); - $table->addForeignKey( - $this->foreignKey() - ->setColumns('user_id') - ->setReferencedTable('users') - ->setReferencedColumns('user_id') - ->setDelete(ForeignKey::CASCADE) - ->setName('article_user_fk') - ) - ->save(); - } -} -``` - -::: info Added in version 4.6.0 -The `foreignKey` method was added. -::: - -We can also easily check if a foreign key exists: - -```php -table('tag_relationships'); - $exists = $table->hasForeignKey('tag_id'); - if ($exists) { - // do something - } - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -Finally, to delete a foreign key, use the `dropForeignKey` method. - -Note that like other methods in the `Table` class, `dropForeignKey` also -needs `save()` to be called at the end in order to be executed. This allows -Migrations to intelligently plan migrations when more than one table is -involved: - -```php -table('tag_relationships'); - $table->dropForeignKey('tag_id')->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -### Working With Check Constraints - -::: info Added in version 5.0.0 -Check constraints were added in 5.0.0. -::: - -Check constraints allow you to enforce data validation rules at the database level. -They are particularly useful for ensuring data integrity across your application. - -> [!NOTE] -> Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite. -> SQL Server support is planned for a future release. - -#### Adding a Check Constraint - -You can add a check constraint to a table using the `addCheckConstraint()` method: - -```php -table('products'); - $table->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) - ->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('products'); - $table->dropCheckConstraint('price_positive') - ->save(); - } -} -``` - -The first argument is the constraint name, and the second is the SQL expression -that defines the constraint. The expression should evaluate to a boolean value. - -#### Using the CheckConstraint Fluent Builder - -For more complex scenarios, you can use the `checkConstraint()` method to get -a fluent builder: - -```php -table('users'); - $table->addColumn('age', 'integer') - ->addColumn('status', 'string', ['limit' => 20]) - ->addCheckConstraint( - $this->checkConstraint() - ->setName('age_valid') - ->setExpression('age >= 18 AND age <= 120') - ) - ->addCheckConstraint( - $this->checkConstraint() - ->setName('status_valid') - ->setExpression("status IN ('active', 'inactive', 'pending')") - ) - ->save(); - } -} -``` - -#### Auto-Generated Constraint Names - -If you don't specify a constraint name, one will be automatically generated based -on the table name and expression hash: - -```php -table('inventory'); - $table->addColumn('quantity', 'integer') - // Name will be auto-generated like 'inventory_chk_a1b2c3d4' - ->addCheckConstraint( - $this->checkConstraint() - ->setExpression('quantity >= 0') - ) - ->save(); - } -} -``` - -#### Complex Check Constraints - -Check constraints can reference multiple columns and use complex SQL expressions: - -```php -table('date_ranges'); - $table->addColumn('start_date', 'date') - ->addColumn('end_date', 'date') - ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2]) - ->addCheckConstraint('valid_date_range', 'end_date >= start_date') - ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100') - ->save(); - } -} -``` - -#### Checking if a Check Constraint Exists - -You can verify if a check constraint exists using the `hasCheckConstraint()` method: - -```php -table('products'); - $exists = $table->hasCheckConstraint('price_positive'); - if ($exists) { - // do something - } else { - $table->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } - } -} -``` - -#### Dropping a Check Constraint - -To remove a check constraint, use the `dropCheckConstraint()` method with the -constraint name: - -```php -table('products'); - $table->dropCheckConstraint('price_positive') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('products'); - $table->addCheckConstraint('price_positive', 'price > 0') - ->save(); - } -} -``` - -> [!NOTE] -> Like other table operations, `dropCheckConstraint()` requires `save()` -> to be called to execute the change. - -#### Database-Specific Behavior - -**MySQL (8.0.16+)** - -Check constraints are fully supported. MySQL stores constraint metadata in the -`INFORMATION_SCHEMA.CHECK_CONSTRAINTS` table. - -**PostgreSQL** - -Check constraints are fully supported and stored in the `pg_constraint` catalog. -PostgreSQL allows the most flexible expressions in check constraints. - -**SQLite** - -Check constraints are supported but with some limitations. SQLite does not support -`ALTER TABLE` operations for check constraints, so adding or dropping constraints -requires recreating the entire table. This is handled automatically by the adapter. - -**SQL Server** - -Check constraint support for SQL Server is planned for a future release. - -### Determining Whether a Table Exists - -You can determine whether or not a table exists by using the `hasTable()` -method: - -```php -hasTable('users'); - if ($exists) { - // do something - } - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -### Dropping a Table - -Tables can be dropped quite easily using the `drop()` method. It is a -good idea to recreate the table again in the `down()` method. - -Note that like other methods in the `Table` class, `drop` also needs `save()` -to be called at the end in order to be executed. This allows Migrations to intelligently -plan migrations when more than one table is involved: - -```php -table('users')->drop()->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->save(); - } -} -``` - -### Renaming a Table - -To rename a table access an instance of the Table object then call the -`rename()` method: - -```php -table('users'); - $table - ->rename('legacy_users') - ->update(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('legacy_users'); - $table - ->rename('users') - ->update(); - } -} -``` - -### Changing the Primary Key - -To change the primary key on an existing table, use the `changePrimaryKey()` -method. Pass in a column name or array of columns names to include in the -primary key, or `null` to drop the primary key. Note that the mentioned -columns must be added to the table, they will not be added implicitly: - -```php -table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->addColumn('new_id', 'integer', ['null' => false]) - ->changePrimaryKey(['new_id', 'username']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -### Creating Custom Primary Keys - -You can specify a `autoId` property in the Migration class and set it to -`false`, which will turn off the automatic `id` column creation. You will -need to manually create the column that will be used as a primary key and add -it to the table declaration: - -```php -table('products'); - $table - ->addColumn('id', 'uuid') - ->addPrimaryKey('id') - ->addColumn('name', 'string') - ->addColumn('description', 'text') - ->create(); - } -} -``` - -The above will create a `CHAR(36)` `id` column that is also the primary key. - -When specifying a custom primary key on the command line, you must note -it as the primary key in the id field, otherwise you may get an error -regarding duplicate id fields, i.e.: - -```bash -bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified -``` - -All baked migrations and snapshot will use this new way when necessary. - -> [!WARNING] -> Dealing with primary key can only be done on table creation operations. -> This is due to limitations for some database servers the plugin supports. - -### Changing the Table Comment - -To change the comment on an existing table, use the `changeComment()` method. -Pass in a string to set as the new table comment, or `null` to drop the existing comment: - -```php -table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->changeComment('This is the table with users auth information, password should be encrypted') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -## Checking Columns - -`BaseMigration` also provides methods for introspecting the current schema, -allowing you to conditionally make changes to schema, or read data. -Schema is inspected **when the migration is run**. - -### Get a column list - -To retrieve all table columns, simply create a `table` object and call -`getColumns()` method. This method will return an array of Column classes with -basic info. Example below: - -```php -table('users')->getColumns(); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } -} -``` - -### Get a column by name - -To retrieve one table column, simply create a `table` object and call the -`getColumn()` method. This method will return a Column class with basic info -or NULL when the column doesn't exist. Example below: - -```php -table('users')->getColumn('email'); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } -} -``` - -### Checking whether a column exists - -You can check if a table already has a certain column by using the -`hasColumn()` method: - -```php -table('user'); - $column = $table->hasColumn('username'); - - if ($column) { - // do something - } - - } -} - - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -### Dropping a Table - -Tables can be dropped quite easily using the `drop()` method. It is a -good idea to recreate the table again in the `down()` method. - -Note that like other methods in the `Table` class, `drop` also needs `save()` -to be called at the end in order to be executed. This allows Migrations to intelligently -plan migrations when more than one table is involved: - -```php -table('users')->drop()->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $users = $this->table('users'); - $users->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->addColumn('password_salt', 'string', ['limit' => 40]) - ->addColumn('email', 'string', ['limit' => 100]) - ->addColumn('first_name', 'string', ['limit' => 30]) - ->addColumn('last_name', 'string', ['limit' => 30]) - ->addColumn('created', 'datetime') - ->addColumn('updated', 'datetime', ['null' => true]) - ->addIndex(['username', 'email'], ['unique' => true]) - ->save(); - } -} -``` - -### Renaming a Table - -To rename a table access an instance of the Table object then call the -`rename()` method: - -```php -table('users'); - $table - ->rename('legacy_users') - ->update(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - $table = $this->table('legacy_users'); - $table - ->rename('users') - ->update(); - } -} -``` - -### Changing the Primary Key - -To change the primary key on an existing table, use the `changePrimaryKey()` -method. Pass in a column name or array of columns names to include in the -primary key, or `null` to drop the primary key. Note that the mentioned -columns must be added to the table, they will not be added implicitly: - -```php -table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20, 'null' => false]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->addColumn('new_id', 'integer', ['null' => false]) - ->changePrimaryKey(['new_id', 'username']) - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -### Creating Custom Primary Keys - -You can specify a `autoId` property in the Migration class and set it to -`false`, which will turn off the automatic `id` column creation. You will -need to manually create the column that will be used as a primary key and add -it to the table declaration: - -```php -table('products'); - $table - ->addColumn('id', 'uuid') - ->addPrimaryKey('id') - ->addColumn('name', 'string') - ->addColumn('description', 'text') - ->create(); - } -} -``` - -The above will create a `CHAR(36)` `id` column that is also the primary key. - -When specifying a custom primary key on the command line, you must note -it as the primary key in the id field, otherwise you may get an error -regarding duplicate id fields, i.e.: - -```bash -bin/cake bake migration CreateProducts id:uuid:primary name:string description:text created modified -``` - -All baked migrations and snapshot will use this new way when necessary. - -> [!WARNING] -> Dealing with primary key can only be done on table creation operations. -> This is due to limitations for some database servers the plugin supports. - -### Changing the Table Comment - -To change the comment on an existing table, use the `changeComment()` method. -Pass in a string to set as the new table comment, or `null` to drop the existing comment: - -```php -table('users'); - $users - ->addColumn('username', 'string', ['limit' => 20]) - ->addColumn('password', 'string', ['limit' => 40]) - ->save(); - - $users - ->changeComment('This is the table with users auth information, password should be encrypted') - ->save(); - } - - /** - * Migrate Down. - */ - public function down(): void - { - - } -} -``` - -## Checking Columns - -`BaseMigration` also provides methods for introspecting the current schema, -allowing you to conditionally make changes to schema, or read data. -Schema is inspected **when the migration is run**. - -### Get a column list - -To retrieve all table columns, simply create a `table` object and call -`getColumns()` method. This method will return an array of Column classes with -basic info. Example below: - -```php -table('users')->getColumns(); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } -} -``` - -### Get a column by name - -To retrieve one table column, simply create a `table` object and call the -`getColumn()` method. This method will return a Column class with basic info -or NULL when the column doesn't exist. Example below: - -```php -table('users')->getColumn('email'); - ... - } - - /** - * Migrate Down. - */ - public function down(): void - { - ... - } -} -``` - -### Checking whether a column exists - -You can check if a table already has a certain column by using the -`hasColumn()` method: - -```php -table('user'); - $column = $table->hasColumn('username'); - - if ($column) { - // do something - } - - } -} -``` - -### Changing templates - -See [Custom Seed Migration Templates](seeding#custom-seed-migration-templates) for how to customize the templates -used to generate migrations. - -## Database-Specific Limitations - -While Migrations aims to provide a database-agnostic API, some features have -database-specific limitations or are not available on all platforms. - -### SQL Server - -The following features are not supported on SQL Server: - -**Check Constraints** - -Check constraints are not currently implemented for SQL Server. Attempting to -use `addCheckConstraint()` or `dropCheckConstraint()` will throw a -`BadMethodCallException`. - -**Table Comments** - -SQL Server does not support table comments. Attempting to use `changeComment()` -will throw a `BadMethodCallException`. - -**INSERT IGNORE / insertOrSkip()** - -SQL Server does not support the `INSERT IGNORE` syntax used by `insertOrSkip()`. -This method will throw a `RuntimeException` on SQL Server. Use `insertOrUpdate()` -instead for upsert operations, which uses `MERGE` statements on SQL Server. - -### SQLite - -**Foreign Key Names** - -SQLite does not support named foreign keys. The foreign key constraint name option -is ignored when creating foreign keys on SQLite. - -**Table Comments** - -SQLite does not support table comments directly. Comments are stored as metadata -but not in the database itself. - -**Check Constraint Modifications** - -SQLite does not support `ALTER TABLE` operations for check constraints. Adding or -dropping check constraints requires recreating the entire table, which is handled -automatically by the adapter. - -**Table Partitioning** - -SQLite does not support table partitioning. - -### PostgreSQL - -**KEY Partitioning** - -PostgreSQL does not support MySQL's `KEY` partitioning type. Use `HASH` -partitioning instead for similar distribution behavior. - -### MySQL/MariaDB - -**insertOrUpdate() Conflict Columns** - -For MySQL, the `$conflictColumns` parameter in `insertOrUpdate()` is ignored -because MySQL's `ON DUPLICATE KEY UPDATE` automatically applies to all unique -constraints. PostgreSQL and SQLite require this parameter to be specified. - -**MariaDB GIS/Geometry** - -Some geometry column features may not work correctly on MariaDB due to differences -in GIS implementation compared to MySQL. diff --git a/docs/en/writing-migrations.rst b/docs/en/writing-migrations.rst deleted file mode 100644 index e69de29bb..000000000 From 1fc7f8d276924da16858bd09353d05b786fad5cd Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 19 Apr 2026 10:16:05 +0200 Subject: [PATCH 40/72] docs: restore reference content lost in docs split (#1073) Follow-up to #1070. The writing-migrations split dropped several reference sections and left some RST-conversion artifacts. - Fix `bin/cake migration migrate` typo in integration-and-deployment - Fix missing space around `phinxlog` tables in upgrade guide - Replace leftover `...` with backticks in seeding and using-the-query-builder - Restore "Anonymous Migration Classes" section in creating-migrations - Restore full Table Partitioning examples (RANGE, LIST, HASH, KEY, expressions, modifying existing partitions) - Restore index examples for MySQL fulltext/length, SQL Server and PostgreSQL include columns, partial indexes, concurrent indexes, and GIN indexes - Restore foreign key details: composite keys, options table, and hasForeignKey/dropForeignKey usage - Restore check constraint details: fluent builder, auto-generated names, complex expressions, has/drop examples --- .../en/advanced/integration-and-deployment.md | 2 +- .../en/getting-started/creating-migrations.md | 48 +++ docs/en/guides/seeding.md | 4 +- docs/en/guides/using-the-query-builder.md | 2 +- .../columns-and-table-operations.md | 178 +++++++++- .../indexes-and-constraints.md | 320 +++++++++++++++--- .../upgrades/upgrading-to-builtin-backend.md | 2 +- 7 files changed, 504 insertions(+), 52 deletions(-) diff --git a/docs/en/advanced/integration-and-deployment.md b/docs/en/advanced/integration-and-deployment.md index 35be96e63..7b1f57b90 100644 --- a/docs/en/advanced/integration-and-deployment.md +++ b/docs/en/advanced/integration-and-deployment.md @@ -192,7 +192,7 @@ columns not existing when performing operations on those new columns. The CakePHP core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you can use: ```bash -bin/cake migration migrate +bin/cake migrations migrate bin/cake schema_cache clear ``` diff --git a/docs/en/getting-started/creating-migrations.md b/docs/en/getting-started/creating-migrations.md index f99f26087..7f8b8e055 100644 --- a/docs/en/getting-started/creating-migrations.md +++ b/docs/en/getting-started/creating-migrations.md @@ -45,6 +45,54 @@ You can also use the `underscore_form` as the name for your migrations, such as > migrations if the class names are not unique. In that case, you may need to > rename the migration manually. +## Anonymous Migration Classes + +Migrations also supports generating anonymous migration classes, which use PHP's +anonymous class feature instead of named classes. This style is useful for: + +- Avoiding namespace declarations +- Better PHPCS compatibility (no class name to filename matching required) +- Simpler file structure without named class constraints +- More readable filenames like `2024_12_08_120000_CreateProducts.php` + +To generate an anonymous migration class, use the `--style anonymous` option: + +```bash +bin/cake bake migration CreateProducts --style anonymous +``` + +This generates a migration file using an anonymous class: + +```php + [ + 'style' => 'anonymous', // or 'traditional' +], +``` + +This configuration also applies to seeds, allowing you to use consistent +styling across your entire project. + ## Creating a Table You can use `bake migration` to create a table: diff --git a/docs/en/guides/seeding.md b/docs/en/guides/seeding.md index f1914aad1..6ceb82823 100644 --- a/docs/en/guides/seeding.md +++ b/docs/en/guides/seeding.md @@ -509,7 +509,7 @@ The method takes three arguments: ## Truncating Tables In addition to inserting data Migrations makes it trivial to empty your tables using the -SQL TRUNCATE command: +SQL `TRUNCATE` command: ```php -v parameter for more output verbosity: +You can also use the `-v` parameter for more output verbosity: ```bash bin/cake seeds run -v diff --git a/docs/en/guides/using-the-query-builder.md b/docs/en/guides/using-the-query-builder.md index 931c2d7dd..1f65cbc17 100644 --- a/docs/en/guides/using-the-query-builder.md +++ b/docs/en/guides/using-the-query-builder.md @@ -7,7 +7,7 @@ Migrations provides access to a Query builder object, that you may use to execut The Query builder is provided by the [cakephp/database](https://github.com/cakephp/database) project, and should be easy to work with as it resembles very closely plain SQL. Accesing the query builder is done by calling the -`getQueryBuilder(string $type)` function. The `string $type` options are 'select', 'insert', 'update' and \`'delete'\`: +`getQueryBuilder(string $type)` function. The `string $type` options are `'select'`, `'insert'`, `'update'` and `'delete'`: ```php [!NOTE] +> Partition columns must be included in the primary key for MySQL. SQLite does +> not support partitioning. MySQL's `RANGE` and `LIST` types only work with +> integer columns - use `RANGE COLUMNS` and `LIST COLUMNS` for DATE/STRING +> columns. + +### RANGE Partitioning + +RANGE partitioning is useful when you want to partition by numeric ranges. For +MySQL, use `TYPE_RANGE` with integer columns or expressions, and +`TYPE_RANGE_COLUMNS` for DATE/DATETIME/STRING columns: + +```php +table('orders', [ + 'id' => false, + 'primary_key' => ['id', 'order_date'], + ]); + $table->addColumn('id', 'integer', ['identity' => true]) + ->addColumn('order_date', 'date') + ->addColumn('amount', 'decimal', ['precision' => 10, 'scale' => 2]) + ->partitionBy(Partition::TYPE_RANGE_COLUMNS, 'order_date') + ->addPartition('p2022', '2023-01-01') + ->addPartition('p2023', '2024-01-01') + ->addPartition('p2024', '2025-01-01') + ->addPartition('pmax', 'MAXVALUE') + ->create(); + } +} +``` + +### LIST Partitioning + +LIST partitioning is useful when you want to partition by discrete values. For +MySQL, use `TYPE_LIST` with integer columns and `TYPE_LIST_COLUMNS` for STRING +columns: + +```php +table('customers', [ + 'id' => false, + 'primary_key' => ['id', 'region'], + ]); + $table->addColumn('id', 'integer', ['identity' => true]) + ->addColumn('region', 'string', ['limit' => 20]) + ->addColumn('name', 'string') + ->partitionBy(Partition::TYPE_LIST_COLUMNS, 'region') + ->addPartition('p_americas', ['US', 'CA', 'MX', 'BR']) + ->addPartition('p_europe', ['UK', 'DE', 'FR', 'IT']) + ->addPartition('p_asia', ['JP', 'CN', 'IN', 'KR']) + ->create(); + } +} +``` + +### HASH Partitioning + +HASH partitioning distributes data evenly across a specified number of +partitions: + +```php +table('sessions'); + $table->addColumn('user_id', 'integer') + ->addColumn('data', 'text') + ->partitionBy(Partition::TYPE_HASH, 'user_id', ['count' => 8]) + ->create(); + } +} +``` -- `RANGE` -- `RANGE COLUMNS` -- `LIST` -- `LIST COLUMNS` -- `HASH` -- `KEY` *(MySQL only)* +### KEY Partitioning (MySQL only) -You can also partition by expressions using `Literal::from(...)`, and add or -drop partitions on existing tables. +KEY partitioning is similar to HASH but uses MySQL's internal hashing function: + +```php +table('cache', [ + 'id' => false, + 'primary_key' => ['cache_key'], + ]); + $table->addColumn('cache_key', 'string', ['limit' => 255]) + ->addColumn('value', 'binary') + ->partitionBy(Partition::TYPE_KEY, 'cache_key', ['count' => 16]) + ->create(); + } +} +``` + +### Partitioning with Expressions + +You can partition by expressions using the `Literal` class: + +```php +table('events', [ + 'id' => false, + 'primary_key' => ['id', 'created_at'], + ]); + $table->addColumn('id', 'integer', ['identity' => true]) + ->addColumn('created_at', 'datetime') + ->partitionBy(Partition::TYPE_RANGE, Literal::from('YEAR(created_at)')) + ->addPartition('p2022', 2023) + ->addPartition('p2023', 2024) + ->addPartition('pmax', 'MAXVALUE') + ->create(); + } +} +``` + +### Modifying Partitions on Existing Tables + +You can add or drop partitions on existing partitioned tables: + +```php +table('orders') + ->addPartitionToExisting('p2025', '2026-01-01') + ->update(); + } + + public function down(): void + { + $this->table('orders') + ->dropPartition('p2025') + ->update(); + } +} +``` ## Saving Changes diff --git a/docs/en/guides/writing-migrations/indexes-and-constraints.md b/docs/en/guides/writing-migrations/indexes-and-constraints.md index 5c7a680db..3ab9115ce 100644 --- a/docs/en/guides/writing-migrations/indexes-and-constraints.md +++ b/docs/en/guides/writing-migrations/indexes-and-constraints.md @@ -2,11 +2,10 @@ ## Working With Indexes -To add an index to a table, call `addIndex()`: +To add an index to a table, call `addIndex()` on the Table object: ```php table('users') + ->addColumn('email', 'string') + ->addColumn('username', 'string') + ->addIndex(['email', 'username'], [ + 'unique' => true, + 'name' => 'idx_users_email', + 'order' => ['email' => 'DESC', 'username' => 'ASC'], + ]) + ->save(); +``` -The fluent builder is also available: +As of 4.6.0, you can use `BaseMigration::index()` to get a fluent builder: ```php $this->table('users') @@ -37,102 +49,334 @@ $this->table('users') ->save(); ``` -Adapter-specific capabilities include: +### MySQL Fulltext Indexes + +The MySQL adapter supports `fulltext` indexes. If you are using a version +before 5.6 the table must use the `MyISAM` engine: + +```php +$table = $this->table('users', ['engine' => 'MyISAM']); +$table->addColumn('email', 'string') + ->addIndex('email', ['type' => 'fulltext']) + ->create(); +``` + +### MySQL Index Length + +The MySQL adapter supports setting the index length via the `limit` option. +For multi-column indexes you can define the length per column: + +```php +$this->table('users') + ->addColumn('email', 'string') + ->addColumn('username', 'string') + ->addColumn('user_guid', 'string', ['limit' => 36]) + ->addIndex(['email', 'username'], ['limit' => ['email' => 5, 'username' => 2]]) + ->addIndex('user_guid', ['limit' => 6]) + ->create(); +``` + +### Include Columns (SQL Server and PostgreSQL) + +The SQL Server and PostgreSQL adapters support `include` (non-key) columns on +indexes: + +```php +$this->table('users') + ->addColumn('email', 'string') + ->addColumn('firstname', 'string') + ->addColumn('lastname', 'string') + ->addIndex(['email'], ['include' => ['firstname', 'lastname']]) + ->create(); +``` + +### Partial Indexes + +PostgreSQL, SQL Server, and SQLite support partial indexes via a `WHERE` +clause: -- MySQL `fulltext` indexes -- MySQL index-length options -- SQL Server and PostgreSQL `include` columns -- PostgreSQL, SQL Server, and SQLite partial indexes -- PostgreSQL concurrent index creation -- PostgreSQL `gin` indexes +```php +$this->table('users') + ->addColumn('email', 'string') + ->addColumn('is_verified', 'boolean') + ->addIndex( + $this->index('email') + ->setName('user_email_verified_idx') + ->setType('unique') + ->setWhere('is_verified = true') + ) + ->create(); +``` -To remove indexes, use `removeIndex()` or `removeIndexByName()`. +### Concurrent Index Creation (PostgreSQL) + +PostgreSQL can create indexes concurrently, which avoids taking disruptive +locks during index creation: + +```php +$this->table('users') + ->addColumn('email', 'string') + ->addIndex( + $this->index('email') + ->setName('user_email_unique_idx') + ->setType('unique') + ->setConcurrently(true) + ) + ->create(); +``` + +### GIN Indexes (PostgreSQL) + +The PostgreSQL adapter also supports Generalized Inverted Index (`gin`) +indexes: + +```php +$this->table('users') + ->addColumn('address', 'string') + ->addIndex('address', ['type' => 'gin']) + ->create(); +``` + +### Removing Indexes + +Use `removeIndex()` with the list of columns, or `removeIndexByName()` if you +named the index: + +```php +$table = $this->table('users'); +$table->removeIndex(['email']) + ->save(); + +$table->removeIndexByName('idx_users_email') + ->save(); +``` + +::: info Added in version 4.6.0 +`Index::setWhere()` and `Index::setConcurrently()` were added. +::: ## Working With Foreign Keys -Migrations supports foreign key constraints on database tables: +Migrations supports creating foreign key constraints on database tables: ```php table('tags') + ->addColumn('tag_name', 'string') + ->save(); + $this->table('tag_relationships') ->addColumn('tag_id', 'integer', ['null' => true]) ->addForeignKey( 'tag_id', 'tags', 'id', - ['delete' => 'SET_NULL', 'update' => 'NO_ACTION'] + ['delete' => 'SET_NULL', 'update' => 'NO_ACTION'], ) ->save(); } } ``` -The `delete` and `update` options control `ON DELETE` and `ON UPDATE` +The `delete` and `update` options define the `ON DELETE` and `ON UPDATE` behavior. Valid values are `SET_NULL`, `NO_ACTION`, `CASCADE`, and -`RESTRICT`. +`RESTRICT`. If `SET_NULL` is used, the column must be created as nullable +with `['null' => true]`. -Foreign keys can also be defined with arrays of columns for composite keys. +### Composite Foreign Keys -The `foreignKey()` fluent builder is available for more complex cases: +Foreign keys can be defined with arrays of columns to build constraints +between tables with composite keys: ```php -$this->table('articles') +$this->table('follower_events') + ->addColumn('user_id', 'integer') + ->addColumn('follower_id', 'integer') + ->addColumn('event_id', 'integer') ->addForeignKey( - $this->foreignKey() - ->setColumns('user_id') - ->setReferencedTable('users') - ->setReferencedColumns('user_id') - ->setName('article_user_fk') + ['user_id', 'follower_id'], + 'followers', + ['user_id', 'follower_id'], + [ + 'delete' => 'NO_ACTION', + 'update' => 'NO_ACTION', + 'constraint' => 'user_follower_id', + ], ) ->save(); ``` -Use `hasForeignKey()` to check whether a foreign key exists, and -`dropForeignKey()` to remove one. +The options parameter of `addForeignKey()` supports the following: + +| Option | Description | +|------------|--------------------------------------------------------| +| update | set an action to be triggered when the row is updated | +| delete | set an action to be triggered when the row is deleted | +| constraint | set a name to be used by foreign key constraint | +| deferrable | define deferred constraint application (postgres only) | + +### Fluent Foreign Key Builder + +`foreignKey()` returns a fluent builder for more complex cases: + +```php +table('articles') + ->addForeignKey( + $this->foreignKey() + ->setColumns('user_id') + ->setReferencedTable('users') + ->setReferencedColumns('user_id') + ->setDelete(ForeignKey::CASCADE) + ->setName('article_user_fk') + ) + ->save(); + } +} +``` + +::: info Added in version 4.6.0 +The `foreignKey` method was added. +::: + +### Checking and Dropping Foreign Keys + +Use `hasForeignKey()` to check whether a foreign key exists: + +```php +$table = $this->table('tag_relationships'); +if ($table->hasForeignKey('tag_id')) { + // do something +} +``` + +To delete a foreign key, use `dropForeignKey()`. Like other Table methods, it +needs `save()` to be called at the end: + +```php +$this->table('tag_relationships') + ->dropForeignKey('tag_id') + ->save(); +``` ## Working With Check Constraints +::: info Added in version 5.0.0 +Check constraints were added in 5.0.0. +::: + Check constraints allow you to enforce data validation rules at the database level. > [!NOTE] > Check constraints are supported by MySQL 8.0.16+, PostgreSQL, and SQLite. +> SQL Server support is planned for a future release. ### Adding a Check Constraint +The first argument is the constraint name, and the second is the SQL +expression that defines the constraint: + ```php -$this->table('products') - ->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) - ->addCheckConstraint('price_positive', 'price > 0') - ->save(); +table('products') + ->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2]) + ->addCheckConstraint('price_positive', 'price > 0') + ->save(); + } + + public function down(): void + { + $this->table('products') + ->dropCheckConstraint('price_positive') + ->save(); + } +} ``` -### Using the Fluent Builder +### Using the CheckConstraint Fluent Builder + +For more complex scenarios, `checkConstraint()` returns a fluent builder: ```php $this->table('users') + ->addColumn('age', 'integer') + ->addColumn('status', 'string', ['limit' => 20]) ->addCheckConstraint( $this->checkConstraint() ->setName('age_valid') ->setExpression('age >= 18 AND age <= 120') ) + ->addCheckConstraint( + $this->checkConstraint() + ->setName('status_valid') + ->setExpression("status IN ('active', 'inactive', 'pending')") + ) ->save(); ``` -If you do not specify a name, one will be auto-generated. +### Auto-Generated Constraint Names -Check constraints can reference multiple columns and use more complex SQL -expressions. +If you do not specify a constraint name, one will be automatically generated +based on the table name and expression hash: -Use `hasCheckConstraint()` to verify existence and `dropCheckConstraint()` to -remove a constraint. +```php +$this->table('inventory') + ->addColumn('quantity', 'integer') + // Name will be auto-generated like 'inventory_chk_a1b2c3d4' + ->addCheckConstraint( + $this->checkConstraint() + ->setExpression('quantity >= 0') + ) + ->save(); +``` + +### Complex Check Constraints + +Check constraints can reference multiple columns and use complex SQL +expressions: + +```php +$this->table('date_ranges') + ->addColumn('start_date', 'date') + ->addColumn('end_date', 'date') + ->addColumn('discount', 'decimal', ['precision' => 5, 'scale' => 2]) + ->addCheckConstraint('valid_date_range', 'end_date >= start_date') + ->addCheckConstraint('valid_discount', 'discount BETWEEN 0 AND 100') + ->save(); +``` + +### Checking and Dropping Check Constraints + +Use `hasCheckConstraint()` to check whether a constraint exists, and +`dropCheckConstraint()` to remove one: + +```php +$table = $this->table('products'); +if ($table->hasCheckConstraint('price_positive')) { + $table->dropCheckConstraint('price_positive') + ->save(); +} +``` ### Database-Specific Behavior diff --git a/docs/en/upgrades/upgrading-to-builtin-backend.md b/docs/en/upgrades/upgrading-to-builtin-backend.md index f6a6f2a19..575446651 100644 --- a/docs/en/upgrades/upgrading-to-builtin-backend.md +++ b/docs/en/upgrades/upgrading-to-builtin-backend.md @@ -160,7 +160,7 @@ To migrate from `phinxlog` tables to the new `cake_migrations` table: ``` 4. **Optionally drop phinx tables**: Your migration history is preserved - by default. Use `--drop-tables` to drop the `phinxlog`tables after + by default. Use `--drop-tables` to drop the `phinxlog` tables after verifying your migrations run correctly. ```bash From d996bd94146edd1711fbf86bd75c1e92aed03475 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 19 Apr 2026 10:20:59 +0200 Subject: [PATCH 41/72] Handle unified migration ledger in test migrator (#1072) --- src/TestSuite/Migrator.php | 16 +++--- tests/TestCase/TestSuite/MigratorTest.php | 68 +++++++++++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/TestSuite/Migrator.php b/src/TestSuite/Migrator.php index c3c3991aa..dd1602498 100644 --- a/src/TestSuite/Migrator.php +++ b/src/TestSuite/Migrator.php @@ -213,7 +213,7 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo /** * Drops the regular tables of the provided connection - * and truncates the phinx tables. + * and truncates the migration metadata tables. * * @param string $connection Connection on which tables are dropped. * @param string[] $skip A fnmatch compatible list of tables to skip. @@ -225,26 +225,26 @@ protected function dropTables(string $connection, array $skip = []): void if ($dropTables !== []) { $this->helper->dropTables($connection, $dropTables); } - $phinxTables = $this->getPhinxTables($connection); - if ($phinxTables !== []) { - $this->helper->truncateTables($connection, $phinxTables); + $migrationTables = $this->getMigrationTables($connection); + if ($migrationTables !== []) { + $this->helper->truncateTables($connection, $migrationTables); } } /** - * Get the list of tables that are phinxlog + * Get the list of migration metadata tables. * * @param string $connection The connection name to operate on. - * @return string[] The list of tables that are not related to phinx in the provided connection. + * @return string[] The list of migration metadata tables in the provided connection. */ - protected function getPhinxTables(string $connection): array + protected function getMigrationTables(string $connection): array { $connection = ConnectionManager::get($connection); assert($connection instanceof Connection); $tables = $connection->getSchemaCollection()->listTables(); return array_filter($tables, function (string $table): bool { - return str_contains($table, 'phinxlog'); + return str_contains($table, 'phinxlog') || $table === 'cake_migrations'; }); } diff --git a/tests/TestCase/TestSuite/MigratorTest.php b/tests/TestCase/TestSuite/MigratorTest.php index 9363dba40..cf2516c19 100644 --- a/tests/TestCase/TestSuite/MigratorTest.php +++ b/tests/TestCase/TestSuite/MigratorTest.php @@ -16,6 +16,7 @@ use Cake\Chronos\ChronosDate; use Cake\Core\Configure; use Cake\Database\Driver\Postgres; +use Cake\Database\Schema\TableSchema; use Cake\Datasource\ConnectionManager; use Cake\TestSuite\ConnectionHelper; use Cake\TestSuite\TestCase; @@ -55,6 +56,47 @@ protected function getMigratorWhereClause(): array : []; } + protected function makeInspectableMigrator(): Migrator + { + return new class () extends Migrator { + /** + * @return array + */ + public function exposedGetMigrationTables(string $connection): array + { + return array_values($this->getMigrationTables($connection)); + } + + /** + * @return array + */ + public function exposedGetNonPhinxTables(string $connection, array $skip = []): array + { + return array_values($this->getNonPhinxTables($connection, $skip)); + } + }; + } + + protected function createPortableTable(string $name, bool $withPlugin = false): void + { + $connection = ConnectionManager::get('test'); + $schema = new TableSchema($name); + $schema + ->addColumn('version', ['type' => 'biginteger', 'null' => false]) + ->addColumn('migration_name', ['type' => 'string', 'limit' => 100, 'null' => false]) + ->addColumn('start_time', ['type' => 'timestamp', 'default' => null, 'null' => true]) + ->addColumn('end_time', ['type' => 'timestamp', 'default' => null, 'null' => true]) + ->addColumn('breakpoint', ['type' => 'boolean', 'default' => false, 'null' => false]); + + if ($withPlugin) { + $schema->addColumn('plugin', ['type' => 'string', 'limit' => 100, 'default' => null, 'null' => true]); + } + + foreach ($schema->createSql($connection) as $statement) { + $connection->execute($statement); + } + } + protected function setUp(): void { parent::setUp(); @@ -95,6 +137,32 @@ public function testMigrateDropTruncate(): void $this->assertCount(0, $connection->selectQuery()->select(['*'])->from('migrator')->execute()->fetchAll()); } + public function testGetMigrationTablesIncludesUnifiedLedger(): void + { + $this->skipIf(Configure::read('Migrations.legacyTables') !== false); + + $connection = ConnectionManager::get('test'); + $connection->execute('CREATE TABLE sample_table (id INT)'); + $this->createPortableTable('cake_migrations', true); + + $migrator = $this->makeInspectableMigrator(); + + $this->assertSame(['cake_migrations'], $migrator->exposedGetMigrationTables('test')); + $this->assertSame(['sample_table'], $migrator->exposedGetNonPhinxTables('test')); + } + + public function testGetMigrationTablesIncludesLegacyLedger(): void + { + $connection = ConnectionManager::get('test'); + $connection->execute('CREATE TABLE sample_table (id INT)'); + $this->createPortableTable('app_phinxlog'); + + $migrator = $this->makeInspectableMigrator(); + + $this->assertSame(['app_phinxlog'], $migrator->exposedGetMigrationTables('test')); + $this->assertSame(['sample_table'], $migrator->exposedGetNonPhinxTables('test')); + } + public function testMigrateDropNoTruncate(): void { $migrator = new Migrator(); From 1dbb2f7d333295ccc6d78a9c8cfa2cab3064c720 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 19 Apr 2026 10:23:25 +0200 Subject: [PATCH 42/72] Add citext column type support for PostgreSQL (#1069) The citext extension type is already supported end-to-end by the Cake core schema dialect (DDL generation and reflection), but the migrations adapter did not expose a constant and rejected the type in isValidColumnType(). Add TYPE_CITEXT to AdapterInterface, a Column::CITEXT alias, and allow it through PostgresAdapter. --- docs/en/writing-migrations.md | 7 +++-- src/Db/Adapter/AdapterInterface.php | 2 ++ src/Db/Adapter/PostgresAdapter.php | 1 + src/Db/Table/Column.php | 3 ++ .../Db/Adapter/PostgresAdapterTest.php | 28 +++++++++++++++++++ .../TestCase/Db/Adapter/SqliteAdapterTest.php | 1 + 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/en/writing-migrations.md b/docs/en/writing-migrations.md index b415820e0..f28df0900 100644 --- a/docs/en/writing-migrations.md +++ b/docs/en/writing-migrations.md @@ -264,8 +264,11 @@ however with the MySQL adapter + MariaDB, the `nativeuuid` type maps to a native uuid column instead of `CHAR(36)` like `uuid` does. In addition, the Postgres adapter supports `interval`, `json`, `jsonb`, -`uuid`, `cidr`, `inet` and `macaddr` column types (PostgreSQL 9.3 and -above). +`uuid`, `cidr`, `inet`, `macaddr` and `citext` column types (PostgreSQL 9.3 +and above). The `citext` type requires the `citext` extension to be enabled +on the database; the migration does not create the extension automatically, +run `CREATE EXTENSION IF NOT EXISTS citext` first (for example from an +earlier migration via `$this->execute(...)`). ### Valid Column Options diff --git a/src/Db/Adapter/AdapterInterface.php b/src/Db/Adapter/AdapterInterface.php index 26dfa543c..48cad38c3 100644 --- a/src/Db/Adapter/AdapterInterface.php +++ b/src/Db/Adapter/AdapterInterface.php @@ -94,6 +94,8 @@ interface AdapterInterface public const TYPE_MACADDR = TableSchemaInterface::TYPE_MACADDR; + public const TYPE_CITEXT = TableSchemaInterface::TYPE_CITEXT; + public const TYPE_INTERVAL = TableSchemaInterface::TYPE_INTERVAL; /** diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 3ff57a1fe..89fc010b1 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -53,6 +53,7 @@ class PostgresAdapter extends AbstractAdapter self::TYPE_CIDR, self::TYPE_INET, self::TYPE_MACADDR, + self::TYPE_CITEXT, self::TYPE_INTERVAL, self::TYPE_BINARY_UUID, self::TYPE_NATIVE_UUID, diff --git a/src/Db/Table/Column.php b/src/Db/Table/Column.php index 0fbee5f80..f49ff559b 100644 --- a/src/Db/Table/Column.php +++ b/src/Db/Table/Column.php @@ -92,6 +92,9 @@ class Column extends DatabaseColumn /** Postgres-only column type */ public const MACADDR = TableSchemaInterface::TYPE_MACADDR; + /** Postgres-only column type, requires the `citext` extension */ + public const CITEXT = TableSchemaInterface::TYPE_CITEXT; + /** Postgres-only column type */ public const INTERVAL = TableSchemaInterface::TYPE_INTERVAL; diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index 8e9fb2530..e6a4e3eaa 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -669,6 +669,34 @@ public function testAddColumnJsonbCompat(): void $this->assertTrue($table->hasColumn('config')); } + /** + * Test that adding a column with the citext type works. + * + * Requires the `citext` extension to be enabled on the database; the setUp + * above creates the extension when missing. + */ + public function testAddColumnCitext(): void + { + $table = new Table('table1', [], $this->adapter); + $table->save(); + $this->assertFalse($table->hasColumn('nickname')); + $table->addColumn('nickname', 'citext', ['null' => true]) + ->save(); + $this->assertTrue($table->hasColumn('nickname')); + + $columns = $this->adapter->getColumns('table1'); + $nickname = null; + foreach ($columns as $column) { + if ($column->getName() === 'nickname') { + $nickname = $column; + break; + } + } + $this->assertNotNull($nickname); + $this->assertSame('citext', $nickname->getType()); + $this->assertTrue($nickname->isNull()); + } + public static function providerAddColumnIdentity(): array { return [ diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index a66031d52..240de94ea 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -2745,6 +2745,7 @@ public static function provideColumnTypesForValidation(): array [SqliteAdapter::TYPE_UUID, true], [SqliteAdapter::TYPE_TIMESTAMP, true], [SqliteAdapter::TYPE_CIDR, false], + [SqliteAdapter::TYPE_CITEXT, false], [SqliteAdapter::TYPE_DECIMAL, true], [SqliteAdapter::TYPE_GEOMETRY, false], [SqliteAdapter::TYPE_INET, false], From 90194f2f4b5cae9c96d621d52c27e0969858d76d Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 30 Apr 2026 15:31:30 +0200 Subject: [PATCH 43/72] Update PHPUnit version requirement to include 13.0 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e4cea556e..7e8ad9207 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "cakephp/bake": "^3.3", "cakephp/cakephp": "^5.3.0", "cakephp/cakephp-codesniffer": "^5.0", - "phpunit/phpunit": "^11.5.3 || ^12.1.3" + "phpunit/phpunit": "^11.5.3 || ^12.1.3 || ^13.0" }, "suggest": { "cakephp/bake": "If you want to generate migrations.", From bc56497bc7fa9a985711fc52e900de5d7419a1e6 Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 30 Apr 2026 15:35:39 +0200 Subject: [PATCH 44/72] Set minimum-stability to stable All required and require-dev dependencies are tagged stable releases. Setting minimum-stability explicitly to stable is cleaner than relying on dev with prefer-stable, and matches the convention used in other cakephp plugins (bake, chronos, authentication, queue, etc.). --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e4cea556e..5a2877d4c 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "cakephp/bake": "If you want to generate migrations.", "dereuromark/cakephp-ide-helper": "If you want to have IDE suggest/autocomplete when creating migrations." }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true, "autoload": { "psr-4": { From 69c3eac80c91201b9b959ddcbb38427bd20f29a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:42:13 +0000 Subject: [PATCH 45/72] Bump postcss from 8.5.8 to 8.5.12 in /docs Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.12. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.8...8.5.12) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 3c6ad61c8..c65b0ab07 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1662,9 +1662,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "funding": [ { "type": "opencollective", From c374fde3f625500d041b058987dff4cece36b968 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 4 May 2026 18:43:47 +0200 Subject: [PATCH 46/72] Merge pull request #1076 from cakephp/feature/status-all Add --all flag to migrations status command --- .../en/advanced/integration-and-deployment.md | 13 ++ .../running-and-managing-migrations.md | 32 +++ src/Command/StatusCommand.php | 192 ++++++++++++++++++ tests/TestCase/Command/CompletionTest.php | 2 +- tests/TestCase/Command/StatusCommandTest.php | 77 +++++++ 5 files changed, 315 insertions(+), 1 deletion(-) diff --git a/docs/en/advanced/integration-and-deployment.md b/docs/en/advanced/integration-and-deployment.md index 7b1f57b90..6fc1036f7 100644 --- a/docs/en/advanced/integration-and-deployment.md +++ b/docs/en/advanced/integration-and-deployment.md @@ -62,6 +62,19 @@ bin/cake migrations status -p PluginName bin/cake migrations migrate -p PluginName ``` +To check the app and every loaded plugin in a single call — a typical need on +deploy gates — use `migrations status --all` instead of looping over plugins +manually: + +```bash +bin/cake migrations status --all +``` + +The command prints a compact per-section summary and exits non-zero when +anything is pending, so it slots straight into a CI step. See +[Checking All Plugins at Once](../getting-started/running-and-managing-migrations#checking-all-plugins-at-once) +for the full description of the output and exit codes. + ## Running Migrations in a Non-shell Environment While typical usage of migrations is from the command line, you can also run diff --git a/docs/en/getting-started/running-and-managing-migrations.md b/docs/en/getting-started/running-and-managing-migrations.md index f3c98fda8..7c51a4f9a 100644 --- a/docs/en/getting-started/running-and-managing-migrations.md +++ b/docs/en/getting-started/running-and-managing-migrations.md @@ -58,6 +58,38 @@ bin/cake migrations status --format json You can also use the `--source`, `--connection`, and `--plugin` options just like for the `migrate` command. +### Checking All Plugins at Once + +The `--all` flag prints the status for the app and every loaded plugin that +ships migrations in a single call: + +```bash +bin/cake migrations status --all +``` + +The default output is a compact summary listing only sections that need +action: + +```text +Summary: 2 of 3 sections require action: + - APP: 2 pending + - Migrator: 1 pending +``` + +When everything is migrated, it collapses to a single +`Summary: all N sections are up to date.` line. Add `-v` to also print the +full per-section migration tables before the summary. + +The exit code reflects the worst state across all sections — `0` when clean, +`3` (`CODE_STATUS_DOWN`) when migrations are pending, `2` +(`CODE_STATUS_MISSING`) when entries in the tracking table no longer have +matching files. This makes `status --all` directly usable as a deploy gate +in CI. + +`--format json` returns one combined object keyed by section name, +e.g. `{"app": [...], "PluginName": [...]}`. `--all` cannot be combined with +`--plugin` or `--cleanup`. + ### Cleaning Up Missing Migrations Sometimes migration files may be deleted from the filesystem but still exist in diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php index 7fb7defcd..39e093125 100644 --- a/src/Command/StatusCommand.php +++ b/src/Command/StatusCommand.php @@ -17,6 +17,7 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; +use Cake\Core\Plugin; use Migrations\Config\ConfigInterface; use Migrations\Db\Adapter\UnifiedMigrationsTableStorage; use Migrations\Migration\ManagerFactory; @@ -64,11 +65,20 @@ protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOption '', 'migrations status -c secondary', 'migrations status -c secondary -f json', + 'migrations status --all', + 'Print a summary for the app and every loaded plugin that has migrations.', + 'Add -v to also print the per-section migration tables.', 'migrations status --cleanup', 'Remove *MISSING* migrations from the migration tracking table', ])->addOption('plugin', [ 'short' => 'p', 'help' => 'The plugin to run migrations for', + ])->addOption('all', [ + 'help' => 'Print a status summary for the app and every loaded plugin that has migrations. ' + . 'Use -v to also print the per-section migration tables. ' + . 'Cannot be combined with --plugin or --cleanup.', + 'boolean' => true, + 'default' => false, ])->addOption('connection', [ 'short' => 'c', 'help' => 'The datasource connection to use', @@ -103,6 +113,22 @@ public function execute(Arguments $args, ConsoleIo $io): ?int /** @var string|null $format */ $format = $args->getOption('format'); $clean = $args->getOption('cleanup'); + $all = (bool)$args->getOption('all'); + + if ($all) { + if ($args->getOption('plugin')) { + $io->err('The --all option cannot be combined with --plugin.'); + + return Command::CODE_ERROR; + } + if ($clean) { + $io->err('The --all option cannot be combined with --cleanup.'); + + return Command::CODE_ERROR; + } + + return $this->executeAll($args, $io, $format); + } $factory = new ManagerFactory([ 'plugin' => $args->getOption('plugin'), @@ -143,6 +169,172 @@ public function execute(Arguments $args, ConsoleIo $io): ?int return Command::CODE_SUCCESS; } + /** + * Execute the status command for the app and every loaded plugin + * that ships migrations. + * + * @param \Cake\Console\Arguments $args The command arguments. + * @param \Cake\Console\ConsoleIo $io The console io. + * @param string|null $format Output format. + * @return int The exit code: CODE_STATUS_MISSING (2) when there are missing entries, + * CODE_STATUS_DOWN (3) when there are pending down migrations, CODE_SUCCESS otherwise. + */ + protected function executeAll(Arguments $args, ConsoleIo $io, ?string $format): int + { + $sections = ['app' => null]; + foreach (Plugin::loaded() as $pluginName) { + $migrationsPath = Plugin::path($pluginName) . 'config' . DS + . $args->getOption('source') . DS; + if (!is_dir($migrationsPath)) { + continue; + } + $sections[$pluginName] = $pluginName; + } + + $verbose = (bool)$args->getOption('verbose'); + $jsonResults = []; + $summary = []; + $exitCode = Command::CODE_SUCCESS; + + foreach ($sections as $label => $plugin) { + $factory = new ManagerFactory([ + 'plugin' => $plugin, + 'source' => $args->getOption('source'), + 'connection' => $args->getOption('connection'), + 'dry-run' => $args->getOption('dry-run'), + ]); + $manager = $factory->createManager($io); + $migrations = $manager->printStatus($format); + + $sectionExit = $this->statusExitCode($migrations); + // Precedence: MISSING > DOWN > SUCCESS — once we see MISSING anywhere, keep it. + if ($sectionExit === self::CODE_STATUS_MISSING) { + $exitCode = self::CODE_STATUS_MISSING; + } elseif ($sectionExit === self::CODE_STATUS_DOWN && $exitCode === Command::CODE_SUCCESS) { + $exitCode = self::CODE_STATUS_DOWN; + } + + $summary[$label] = $this->countActions($migrations); + + if ($format === 'json') { + $jsonResults[$label] = $migrations; + continue; + } + + if (!$verbose) { + continue; + } + + $heading = $label === 'app' ? 'APP' : $label; + $io->out(''); + $io->out('=================================================='); + $io->out(sprintf('%s', $heading)); + $io->out('=================================================='); + $this->display($migrations, $io, $manager->getSchemaTableName()); + } + + if ($format === 'json') { + $flags = 0; + if ($verbose) { + $flags = JSON_PRETTY_PRINT; + } + $io->out((string)json_encode($jsonResults, $flags)); + + return $exitCode; + } + + $this->displaySummary($io, $summary); + + return $exitCode; + } + + /** + * Count actionable items (down + missing) in a section's migrations array. + * + * @param array $migrations The result of {@see Manager::printStatus()}. + * @return array{down: int, missing: int} + */ + protected function countActions(array $migrations): array + { + $down = 0; + $missing = 0; + foreach ($migrations as $migration) { + if (!empty($migration['missing'])) { + $missing++; + continue; + } + if (($migration['status'] ?? null) === 'down') { + $down++; + } + } + + return ['down' => $down, 'missing' => $missing]; + } + + /** + * Render the trailing summary block listing sections that need action. + * + * @param \Cake\Console\ConsoleIo $io The console io. + * @param array $summary + * @return void + */ + protected function displaySummary(ConsoleIo $io, array $summary): void + { + $needsAction = array_filter( + $summary, + fn(array $counts): bool => $counts['down'] > 0 || $counts['missing'] > 0, + ); + + $io->out(''); + if (!$needsAction) { + $io->out(sprintf( + 'Summary: all %d sections are up to date.', + count($summary), + )); + + return; + } + + $io->out(sprintf( + 'Summary: %d of %d sections require action:', + count($needsAction), + count($summary), + )); + foreach ($needsAction as $label => $counts) { + $heading = $label === 'app' ? 'APP' : $label; + $parts = []; + if ($counts['down'] > 0) { + $parts[] = sprintf('%d pending', $counts['down']); + } + if ($counts['missing'] > 0) { + $parts[] = sprintf('%d missing', $counts['missing']); + } + $io->out(sprintf(' - %s: %s', $heading, implode(', ', $parts))); + } + } + + /** + * Compute the appropriate status exit code for a single section's migrations array. + * + * @param array $migrations The result of {@see Manager::printStatus()}. + * @return int CODE_STATUS_MISSING when missing entries exist, CODE_STATUS_DOWN when + * any migration is pending (down), otherwise CODE_SUCCESS. + */ + protected function statusExitCode(array $migrations): int + { + $hasDown = false; + foreach ($migrations as $migration) { + if (!empty($migration['missing'])) { + return self::CODE_STATUS_MISSING; + } + if (($migration['status'] ?? null) === 'down') { + $hasDown = true; + } + } + + return $hasDown ? self::CODE_STATUS_DOWN : Command::CODE_SUCCESS; + } + /** * Print migration status to stdout. * diff --git a/tests/TestCase/Command/CompletionTest.php b/tests/TestCase/Command/CompletionTest.php index a1917c42f..d61686dcb 100644 --- a/tests/TestCase/Command/CompletionTest.php +++ b/tests/TestCase/Command/CompletionTest.php @@ -134,7 +134,7 @@ public function testMigrationsOptionsStatus(): void $this->exec('completion options migrations.migrations status'); $this->assertCount(1, $this->_out->messages()); $output = $this->_out->messages()[0]; - $expected = '--cleanup --connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v'; + $expected = '--all --cleanup --connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v'; $outputExplode = explode(' ', trim($output)); sort($outputExplode); $expectedExplode = explode(' ', $expected); diff --git a/tests/TestCase/Command/StatusCommandTest.php b/tests/TestCase/Command/StatusCommandTest.php index 6b3b2f7e1..929b07071 100644 --- a/tests/TestCase/Command/StatusCommandTest.php +++ b/tests/TestCase/Command/StatusCommandTest.php @@ -5,6 +5,7 @@ use Cake\Console\TestSuite\StubConsoleOutput; use Cake\Core\Exception\MissingPluginException; +use Migrations\Command\StatusCommand; use Migrations\Test\TestCase\TestCase; use RuntimeException; @@ -105,4 +106,80 @@ public function testCleanHelp(): void $this->assertOutputContains('--cleanup'); $this->assertOutputContains('Remove MISSING migrations from the'); } + + public function testAllHelp(): void + { + $this->exec('migrations status --help'); + $this->assertExitSuccess(); + $this->assertOutputContains('--all'); + $this->assertOutputContains('every loaded plugin'); + } + + public function testAllAppOnlyExitCodeDownWhenPending(): void + { + $this->exec('migrations status -c test --all'); + // App has unmigrated migrations, so the exit code signals pending. + $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN); + // Default output is summary only — no per-section table. + $this->assertOutputContains('Summary:'); + $this->assertOutputContains('APP:'); + $this->assertOutputContains('pending'); + $this->assertOutputNotContains('Migration ID'); + } + + public function testAllIncludesLoadedPluginWithMigrations(): void + { + $this->loadPlugins(['Migrator']); + $this->exec('migrations status -c test --all'); + $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN); + $this->assertOutputContains('Summary:'); + $this->assertOutputContains('- APP:'); + $this->assertOutputContains('- Migrator:'); + $this->assertOutputNotContains('Plugin:'); + } + + public function testAllVerboseShowsPerSectionTables(): void + { + $this->loadPlugins(['Migrator']); + $this->exec('migrations status -c test --all -v'); + $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN); + $this->assertOutputContains('Migration ID'); + // Plugin section header reads as just the plugin name now. + $this->assertOutputContains('Migrator'); + $this->assertOutputNotContains('Plugin: Migrator'); + // Summary still rendered after the tables. + $this->assertOutputContains('Summary:'); + } + + public function testAllJsonOutput(): void + { + $this->loadPlugins(['Migrator']); + $this->exec('migrations status -c test --all --format json'); + $this->assertExitCode(StatusCommand::CODE_STATUS_DOWN); + + assert($this->_out instanceof StubConsoleOutput); + $messages = $this->_out->messages(); + $jsonLine = end($messages); + $parsed = json_decode((string)$jsonLine, true); + $this->assertIsArray($parsed); + $this->assertArrayHasKey('app', $parsed); + $this->assertArrayHasKey('Migrator', $parsed); + $this->assertIsArray($parsed['app']); + $this->assertIsArray($parsed['Migrator']); + } + + public function testAllRejectsPluginOption(): void + { + $this->loadPlugins(['Migrator']); + $this->exec('migrations status -c test --all -p Migrator'); + $this->assertExitError(); + $this->assertErrorContains('cannot be combined with --plugin'); + } + + public function testAllRejectsCleanupOption(): void + { + $this->exec('migrations status -c test --all --cleanup'); + $this->assertExitError(); + $this->assertErrorContains('cannot be combined with --cleanup'); + } } From 9fc2d25d221faf14c421d968309229e2c6f746ef Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 10 May 2026 08:42:05 +0200 Subject: [PATCH 47/72] Fix testCreateTableIndexWithWhere to look up the query by content (#1085) The test was hardcoding queries[2] to find the CREATE UNIQUE INDEX output. The dryrun message buffer now contains additional phinxlog setup messages before the create-index call, so the position drifted and the test failed locally. Look the query up by its expected prefix instead, which makes the test robust to ordering changes around it. --- tests/TestCase/Db/Adapter/SqliteAdapterTest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index a66031d52..bd90a7e54 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -378,7 +378,13 @@ public function testCreateTableIndexWithWhere(): void ->addIndex($index) ->save(); $queries = $this->out->messages(); - $indexQuery = $queries[2]; + $indexQuery = ''; + foreach ($queries as $query) { + if (str_contains($query, 'CREATE UNIQUE INDEX "table1_email_index"')) { + $indexQuery = $query; + break; + } + } $this->assertStringContainsString('CREATE UNIQUE INDEX "table1_email_index"', $indexQuery); $this->assertStringContainsString('("email" ASC) WHERE is_verified = true', $indexQuery); } From e62390e58d11b3670e5ed4cfb4d3a0ed519891f5 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 13 May 2026 16:45:44 +0200 Subject: [PATCH 48/72] Reduce PHPStan baseline by addressing actual issues (#1083) * Reduce PHPStan baseline by addressing actual issues Remove 13 stale baseline entries by fixing the underlying issues: - BakeSeedCommand: drop wrong @var annotation, cast option value; rewrite reference foreach to track removable keys explicitly - AbstractAdapter: tighten generateBulkInsertSql @param type and cast keys to string for array_map - AdapterFactory: declare final constructor (allows safe new static) - MysqlAdapter: drop redundant nested isset() checks - Environment: useTransactions() is on the interface, drop the dead method_exists guard; gate up()/down() dispatch with method_exists - Manager: cast strpos result, fall back to 0 in max() - TableFinder: drop redundant isset checks on always-set list keys - MigrationHelper: rewrite as if/else so PHPStan can narrow the type Two entries remain in the baseline: - Index::setUnique dynamic dispatch (handled via continue) - SqliteAdapter null check kept for test mock compatibility * Apply rector fixes Match parent visibility on inherited buildOptionParser overrides, type the closure parameter, simplify if/else to ternary, and add the newline rector wants. --- phpstan-baseline.neon | 78 -------------------- src/Command/BakeSeedCommand.php | 15 ++-- src/Command/BakeSimpleMigrationCommand.php | 2 +- src/Db/Adapter/AbstractAdapter.php | 8 +- src/Db/Adapter/AdapterFactory.php | 7 ++ src/Db/Adapter/MysqlAdapter.php | 4 +- src/Migration/Environment.php | 7 +- src/Migration/Manager.php | 16 +++- src/Util/TableFinder.php | 4 +- src/View/Helper/MigrationHelper.php | 5 +- tests/TestCase/Migration/EnvironmentTest.php | 3 +- 11 files changed, 39 insertions(+), 110 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9a7200326..0d7ad0789 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,35 +1,5 @@ parameters: ignoreErrors: - - - message: '#^PHPDoc tag @var with type string is not subtype of native type non\-falsy\-string\|true\.$#' - identifier: varTag.nativeType - count: 1 - path: src/Command/BakeSeedCommand.php - - - - message: '#^Strict comparison using \!\=\= between string and false will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: src/Command/BakeSeedCommand.php - - - - message: '#^Parameter \#1 \$callback of function array_map expects \(callable\(int\|string\)\: mixed\)\|null, Closure\(string\)\: string given\.$#' - identifier: argument.type - count: 1 - path: src/Db/Adapter/AbstractAdapter.php - - - - message: '#^Unsafe usage of new static\(\)\.$#' - identifier: new.static - count: 1 - path: src/Db/Adapter/AdapterFactory.php - - - - message: '#^Offset ''id'' on non\-empty\-array\ in isset\(\) always exists and is not nullable\.$#' - identifier: isset.offset - count: 2 - path: src/Db/Adapter/MysqlAdapter.php - - message: '#^Strict comparison using \!\=\= between Cake\\Database\\StatementInterface and null will always evaluate to true\.$#' identifier: notIdentical.alwaysTrue @@ -41,51 +11,3 @@ parameters: identifier: method.notFound count: 1 path: src/Db/Table/Index.php - - - - message: '#^Call to an undefined method Migrations\\MigrationInterface\:\:down\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Migration/Environment.php - - - - message: '#^Call to an undefined method Migrations\\MigrationInterface\:\:up\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/Migration/Environment.php - - - - message: '#^Call to function method_exists\(\) with Migrations\\MigrationInterface and ''useTransactions'' will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: src/Migration/Environment.php - - - - message: '#^Method Migrations\\Migration\\Manager\:\:getMigrationClassName\(\) should return class\-string\ but returns string\.$#' - identifier: return.type - count: 2 - path: src/Migration/Manager.php - - - - message: '#^Parameter \#1 \.\.\.\$arg1 of function max expects non\-empty\-array, array given\.$#' - identifier: argument.type - count: 1 - path: src/Migration/Manager.php - - - - message: '#^Parameter \#3 \$length of function substr expects int\|null, int\<0, max\>\|false given\.$#' - identifier: argument.type - count: 1 - path: src/Migration/Manager.php - - - - message: '#^Offset 0 on non\-empty\-list\ in isset\(\) always exists and is not nullable\.$#' - identifier: isset.offset - count: 2 - path: src/Util/TableFinder.php - - - - message: '#^Possibly invalid array key type Cake\\Database\\Schema\\TableSchemaInterface\|string\.$#' - identifier: offsetAccess.invalidOffset - count: 2 - path: src/View/Helper/MigrationHelper.php diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index 4129df76f..cfc2d2e35 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -116,8 +116,7 @@ public function templateData(Arguments $arguments): array if ($arguments->getOption('data')) { $limit = (int)$arguments->getOption('limit'); - /** @var string $fields */ - $fields = $arguments->getOption('fields') ?: '*'; + $fields = (string)$arguments->getOption('fields') ?: '*'; if ($fields !== '*') { $fields = explode(',', $fields); } @@ -173,7 +172,7 @@ protected function bake(string $name, Arguments $args, ConsoleIo $io): void * @param \Cake\Console\ConsoleOptionParser $parser Option parser to update. * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser = parent::buildOptionParser($parser); @@ -214,6 +213,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent $lines = explode("\n", $content); $inString = false; + $removeKeys = []; foreach ($lines as $k => &$line) { if ($k === 0) { @@ -237,7 +237,7 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent $tabCount--; } elseif (preg_match("/^\d+\s\=\>\s$/", $line)) { // Mark '0 =>' kind of lines to remove - $line = false; + $removeKeys[] = $k; continue; } @@ -264,10 +264,9 @@ protected function prettifyArray(array $array, int $tabCount = 3, string $indent } unset($line); - // Remove marked lines - $lines = array_filter($lines, function ($line): bool { - return $line !== false; - }); + foreach ($removeKeys as $key) { + unset($lines[$key]); + } return implode("\n", $lines); } diff --git a/src/Command/BakeSimpleMigrationCommand.php b/src/Command/BakeSimpleMigrationCommand.php index d99b63a76..b85f0f7e3 100644 --- a/src/Command/BakeSimpleMigrationCommand.php +++ b/src/Command/BakeSimpleMigrationCommand.php @@ -236,7 +236,7 @@ protected function getMigrationName(?string $name = null): string * @param \Cake\Console\ConsoleOptionParser $parser Option parser to update. * @return \Cake\Console\ConsoleOptionParser */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser = $this->_setCommonOptions($parser); diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 052a74e30..ee76ee1a6 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -841,7 +841,7 @@ public function bulkinsert( * Generates the SQL for a bulk insert. * * @param \Migrations\Db\Table\TableMetadata $table The table to insert into - * @param array $rows The rows to insert + * @param array> $rows The rows to insert * @param \Migrations\Db\InsertMode|null $mode Insert mode * @param array|null $updateColumns Columns to update on upsert conflict * @param array|null $conflictColumns Columns that define uniqueness for upsert (unused in MySQL) @@ -859,15 +859,15 @@ protected function generateBulkInsertSql( $this->getInsertPrefix($mode), $this->quoteTableName($table->getName()), ); - $current = current($rows); - $keys = array_keys($current); + $current = (array)current($rows); + $keys = array_map(strval(...), array_keys($current)); $sql .= '(' . implode(', ', array_map($this->quoteColumnName(...), $keys)) . ') VALUES '; $upsertClause = $this->getUpsertClause($mode, $updateColumns, $conflictColumns); if ($this->isDryRunEnabled()) { - $values = array_map(function ($row): string { + $values = array_map(function (array $row): string { return '(' . implode(', ', array_map($this->quoteValue(...), $row)) . ')'; }, $rows); diff --git a/src/Db/Adapter/AdapterFactory.php b/src/Db/Adapter/AdapterFactory.php index b0dafb929..43c5e573e 100644 --- a/src/Db/Adapter/AdapterFactory.php +++ b/src/Db/Adapter/AdapterFactory.php @@ -23,6 +23,13 @@ class AdapterFactory */ protected static ?AdapterFactory $instance = null; + /** + * Constructor. + */ + final public function __construct() + { + } + /** * Get the factory singleton instance. * diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index db13ed171..f98148f76 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -300,11 +300,11 @@ public function createTable(TableMetadata $table, array $columns = [], array $in ); // Add the default primary key - if (!isset($options['id']) || (isset($options['id']) && $options['id'] === true)) { + if (!isset($options['id']) || $options['id'] === true) { $options['id'] = 'id'; } - if (isset($options['id']) && is_string($options['id'])) { + if (is_string($options['id'])) { $useUnsigned = (bool)Configure::read('Migrations.unsigned_primary_keys'); // Handle id => "field_name" to support AUTO_INCREMENT $column = new Column(); diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 2dd7de09d..80d8437a3 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -67,10 +67,7 @@ public function executeMigration(MigrationInterface $migration, string $directio $migration->{MigrationInterface::INIT}(); } - $atomic = $adapter->hasTransactions(); - if (method_exists($migration, 'useTransactions')) { - $atomic = $migration->useTransactions(); - } + $atomic = $migration->useTransactions(); // begin the transaction if the adapter supports it if ($atomic) { $adapter->beginTransaction(); @@ -97,7 +94,7 @@ public function executeMigration(MigrationInterface $migration, string $directio } else { $migration->{MigrationInterface::CHANGE}(); } - } else { + } elseif (method_exists($migration, $direction)) { $migration->{$direction}(); } } diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 8b77bfc4c..8b011c373 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -322,7 +322,8 @@ public function markMigrated(int $version, string $path): bool * Resolves a migration class name based on $path * * @param string $path Path to the migration file of which we want the class name - * @return class-string<\Migrations\MigrationInterface> Migration class name + * @return string Migration class name + * @phpstan-return class-string<\Migrations\MigrationInterface> */ protected function getMigrationClassName(string $path): string { @@ -330,10 +331,16 @@ protected function getMigrationClassName(string $path): string $class = str_replace('_', ' ', $class); $class = ucwords($class); $class = str_replace(' ', '', $class); - if (str_contains($class, '.')) { - return substr($class, 0, strpos($class, '.')); + + $dotPos = strpos($class, '.'); + if ($dotPos !== false) { + /** @var class-string<\Migrations\MigrationInterface> $name */ + $name = substr($class, 0, $dotPos); + + return $name; } + /** @var class-string<\Migrations\MigrationInterface> $class */ return $class; } @@ -448,7 +455,8 @@ public function migrate(?int $version = null, bool $fake = false, ?int $count = } if ($version === null) { - $version = max(array_merge($versions, array_keys($migrations))); + $candidates = [...$versions, ...array_keys($migrations)]; + $version = $candidates ? max($candidates) : 0; } elseif ($version !== 0 && !isset($migrations[$version])) { $this->getIo()->out(sprintf( 'warning %s is not a valid version', diff --git a/src/Util/TableFinder.php b/src/Util/TableFinder.php index a15d49171..27da85d94 100644 --- a/src/Util/TableFinder.php +++ b/src/Util/TableFinder.php @@ -81,7 +81,7 @@ public function getTablesToBake(CollectionInterface $collection, array $options $config = (array)ConnectionManager::getConfig($this->connection); $key = isset($config['schema']) ? 'schema' : 'database'; - if (isset($split[0], $split[1]) && $config[$key] === $split[1]) { + if (isset($split[1]) && $config[$key] === $split[1]) { $table = $split[0]; } } @@ -191,7 +191,7 @@ public function fetchTableName(string $className, ?string $pluginName = null): a $config = ConnectionManager::getConfig($this->connection); if (is_array($config)) { $key = isset($config['schema']) ? 'schema' : 'database'; - if (isset($splitted[0]) && $config[$key] === $splitted[1]) { + if ($config[$key] === $splitted[1]) { $tableName = $splitted[0]; } } diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index e334ec3f6..3a6c52560 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -708,10 +708,7 @@ public function getCreateTablesElementData(array $tables): array 'tables' => [], ]; foreach ($tables as $table) { - $tableName = $table; - if ($table instanceof TableSchemaInterface) { - $tableName = $table->name(); - } + $tableName = $table instanceof TableSchemaInterface ? $table->name() : $table; $data = $this->getCreateTableData($table); $tableConstraintsNoUnique = array_filter( $data['constraints'], diff --git a/tests/TestCase/Migration/EnvironmentTest.php b/tests/TestCase/Migration/EnvironmentTest.php index 633c20f8b..febadbf7e 100644 --- a/tests/TestCase/Migration/EnvironmentTest.php +++ b/tests/TestCase/Migration/EnvironmentTest.php @@ -204,8 +204,7 @@ public function testExecutingAMigrationWithUseTransactions(): void $adapterStub->expects($this->never()) ->method('commitTransaction'); - $adapterStub->expects($this->atLeastOnce()) - ->method('hasTransactions') + $adapterStub->method('hasTransactions') ->willReturn(true); $this->environment->setAdapter($adapterStub); From 78a1e6b98ac17cf1b5ed14563ff43af64574923f Mon Sep 17 00:00:00 2001 From: PioterLearns Date: Fri, 22 May 2026 00:10:09 +0530 Subject: [PATCH 49/72] Update broken book links (#1087) --- docs/en/advanced/integration-and-deployment.md | 2 +- docs/en/getting-started/creating-migrations.md | 2 +- docs/en/guides/seeding.md | 4 ++-- src/Command/BakeMigrationCommand.php | 2 +- templates/bake/Seed/seed-anonymous.twig | 2 +- templates/bake/Seed/seed.twig | 2 +- templates/bake/config/diff.twig | 4 ++-- templates/bake/config/skeleton-anonymous.twig | 2 +- templates/bake/config/skeleton.twig | 2 +- templates/bake/config/snapshot.twig | 6 +++--- tests/comparisons/Create/TestCreateChange.php | 2 +- .../Diff/addRemove/the_diff_add_remove_mysql.php | 4 ++-- .../Diff/addRemove/the_diff_add_remove_pgsql.php | 4 ++-- .../Diff/decimalChange/initial_decimal_change_mysql.php | 4 ++-- .../decimalChange/mysql/the_diff_decimal_change_mysql.php | 4 ++-- tests/comparisons/Diff/default/the_diff_default_mysql.php | 4 ++-- tests/comparisons/Diff/default/the_diff_default_pgsql.php | 4 ++-- tests/comparisons/Diff/simple/the_diff_simple_mysql.php | 4 ++-- tests/comparisons/Diff/simple/the_diff_simple_pgsql.php | 4 ++-- ...ff_with_auto_id_compatible_signed_primary_keys_mysql.php | 4 ++-- ..._with_auto_id_incompatible_signed_primary_keys_mysql.php | 4 ++-- ...ith_auto_id_incompatible_unsigned_primary_keys_mysql.php | 4 ++-- .../pgsql/test_snapshot_auto_id_disabled_pgsql.php | 4 ++-- .../Migration/pgsql/test_snapshot_not_empty_pgsql.php | 4 ++-- .../Migration/pgsql/test_snapshot_plugin_blog_pgsql.php | 4 ++-- .../Migration/pgsql/test_snapshot_with_change_pgsql.php | 2 +- .../sqlite/test_snapshot_auto_id_disabled_sqlite.php | 4 ++-- .../Migration/sqlite/test_snapshot_not_empty_sqlite.php | 4 ++-- .../Migration/sqlite/test_snapshot_plugin_blog_sqlite.php | 4 ++-- .../Migration/sqlite/test_snapshot_with_change_sqlite.php | 2 +- .../sqlserver/test_snapshot_auto_id_disabled_sqlserver.php | 4 ++-- .../sqlserver/test_snapshot_not_empty_sqlserver.php | 4 ++-- .../sqlserver/test_snapshot_plugin_blog_sqlserver.php | 4 ++-- .../sqlserver/test_snapshot_with_change_sqlserver.php | 2 +- tests/comparisons/Migration/testAddFieldWithReference.php | 2 +- tests/comparisons/Migration/testCreate.php | 2 +- tests/comparisons/Migration/testCreateDatetime.php | 2 +- tests/comparisons/Migration/testCreateDropMigration.php | 2 +- tests/comparisons/Migration/testCreateFieldLength.php | 2 +- tests/comparisons/Migration/testCreatePrimaryKey.php | 2 +- tests/comparisons/Migration/testCreatePrimaryKeyUuid.php | 2 +- tests/comparisons/Migration/testCreateWithReferences.php | 2 +- .../Migration/testCreateWithReferencesCustomTable.php | 2 +- tests/comparisons/Migration/testNoContents.php | 2 +- .../Migration/test_snapshot_auto_id_disabled.php | 4 ++-- tests/comparisons/Migration/test_snapshot_not_empty.php | 4 ++-- tests/comparisons/Migration/test_snapshot_plugin_blog.php | 4 ++-- .../Migration/test_snapshot_postgres_timestamp_tz_pgsql.php | 4 ++-- ...snapshot_with_auto_id_compatible_signed_primary_keys.php | 4 ++-- ...apshot_with_auto_id_incompatible_signed_primary_keys.php | 4 ++-- ...shot_with_auto_id_incompatible_unsigned_primary_keys.php | 4 ++-- tests/comparisons/Migration/test_snapshot_with_change.php | 2 +- .../Migration/test_snapshot_with_non_default_collation.php | 4 ++-- tests/comparisons/Seeds/pgsql/testWithData.php | 2 +- tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php | 2 +- tests/comparisons/Seeds/php81/testWithData.php | 2 +- tests/comparisons/Seeds/php81/testWithDataAndLimit.php | 2 +- tests/comparisons/Seeds/sqlserver/testWithData.php | 2 +- tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php | 2 +- tests/comparisons/Seeds/testBasicBaking.php | 2 +- tests/comparisons/Seeds/testPrettifyArray.php | 2 +- tests/comparisons/Seeds/testWithData.php | 2 +- tests/comparisons/Seeds/testWithDataAndFields.php | 2 +- tests/comparisons/Seeds/testWithDataAndLimit.php | 2 +- .../Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php | 2 +- .../TestBlog/config/CallSeeds/PluginSubLettersSeed.php | 2 +- tests/test_app/config/AltSeeds/AnotherNumbersSeed.php | 2 +- tests/test_app/config/AltSeeds/NumbersAltSeed.php | 2 +- .../2024_12_08_150000_CreateTestAnonymousTable.php | 2 +- tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php | 2 +- tests/test_app/config/CallSeeds/DatabaseSeed.php | 2 +- tests/test_app/config/CallSeeds/LettersSeed.php | 2 +- tests/test_app/config/CallSeeds/NumbersCallSeed.php | 2 +- tests/test_app/config/CallSeeds/ShortNameCallSeed.php | 2 +- .../20160128183623_AlterArticlesDefault.php | 2 +- .../20160128183652_AlterArticlesSlugDefault.php | 2 +- .../20160128183952_CreateUsersDefault.php | 2 +- .../20160128184109_AlterArticlesFkDefault.php | 2 +- .../20160414193900_CreateTagsDefault.php | 2 +- .../20160128183623_AlterArticlesSimple.php | 2 +- ...180516025208_create_test_index_limit_specifier_table.php | 2 +- .../20190928220334_add_column_index_fk.php | 2 +- tests/test_app/config/Seeds/AnonymousStoreSeed.php | 2 +- 83 files changed, 115 insertions(+), 115 deletions(-) diff --git a/docs/en/advanced/integration-and-deployment.md b/docs/en/advanced/integration-and-deployment.md index 6fc1036f7..4426968d9 100644 --- a/docs/en/advanced/integration-and-deployment.md +++ b/docs/en/advanced/integration-and-deployment.md @@ -202,7 +202,7 @@ the application code becomes active. After running migrations, remember to clear the ORM cache so it renews the column metadata of your tables. Otherwise, you might end up with errors about columns not existing when performing operations on those new columns. The -CakePHP core includes a [Schema Cache Shell](https://book.cakephp.org/5/en/console-and-shells/schema-cache.html) that you can use: +CakePHP core includes a [Schema Cache Shell](https://book.cakephp.org/5/console-commands/schema-cache.html) that you can use: ```bash bin/cake migrations migrate diff --git a/docs/en/getting-started/creating-migrations.md b/docs/en/getting-started/creating-migrations.md index 7f8b8e055..ae18c9088 100644 --- a/docs/en/getting-started/creating-migrations.md +++ b/docs/en/getting-started/creating-migrations.md @@ -113,7 +113,7 @@ class CreateProducts extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/writing-migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * @return void */ public function change(): void diff --git a/docs/en/guides/seeding.md b/docs/en/guides/seeding.md index 6ceb82823..be61f7e72 100644 --- a/docs/en/guides/seeding.md +++ b/docs/en/guides/seeding.md @@ -31,7 +31,7 @@ class MyNewSeed extends BaseSeed * Write your database seed using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run() : void { @@ -88,7 +88,7 @@ return new class extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index 8b59fc715..66fb0580e 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -92,7 +92,7 @@ public function templateData(Arguments $arguments): array $action = $this->detectAction($className); if (!$action && count($fields)) { - $this->io->abort('When applying fields the migration name should start with one of the following prefixes: `Create`, `Drop`, `Add`, `Remove`, `Alter`. See: https://book.cakephp.org/migrations/5/en/index.html#migrations-file-name'); + $this->io->abort('When applying fields the migration name should start with one of the following prefixes: `Create`, `Drop`, `Add`, `Remove`, `Alter`. See: https://book.cakephp.org/migrations/5/getting-started/creating-migrations.html#migration-file-names'); } if (!$action) { diff --git a/templates/bake/Seed/seed-anonymous.twig b/templates/bake/Seed/seed-anonymous.twig index b4e0bbaf4..207bce9d8 100644 --- a/templates/bake/Seed/seed-anonymous.twig +++ b/templates/bake/Seed/seed-anonymous.twig @@ -26,7 +26,7 @@ return new class extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/templates/bake/Seed/seed.twig b/templates/bake/Seed/seed.twig index 17b079d47..34c144787 100644 --- a/templates/bake/Seed/seed.twig +++ b/templates/bake/Seed/seed.twig @@ -29,7 +29,7 @@ class {{ name }}Seed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/templates/bake/config/diff.twig b/templates/bake/config/diff.twig index 20514d2a2..1698f252a 100644 --- a/templates/bake/config/diff.twig +++ b/templates/bake/config/diff.twig @@ -31,7 +31,7 @@ class {{ name }} extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -118,7 +118,7 @@ not empty %} * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/templates/bake/config/skeleton-anonymous.twig b/templates/bake/config/skeleton-anonymous.twig index 1d19b868a..93af171db 100644 --- a/templates/bake/config/skeleton-anonymous.twig +++ b/templates/bake/config/skeleton-anonymous.twig @@ -32,7 +32,7 @@ return new class extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * @return void */ public function change(): void diff --git a/templates/bake/config/skeleton.twig b/templates/bake/config/skeleton.twig index 745aeb8d3..e62d5a92f 100644 --- a/templates/bake/config/skeleton.twig +++ b/templates/bake/config/skeleton.twig @@ -32,7 +32,7 @@ class {{ name }} extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/templates/bake/config/snapshot.twig b/templates/bake/config/snapshot.twig index 42296d4e5..f5621fa8d 100644 --- a/templates/bake/config/snapshot.twig +++ b/templates/bake/config/snapshot.twig @@ -35,7 +35,7 @@ class {{ name }} extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ @@ -52,7 +52,7 @@ class {{ name }} extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -69,7 +69,7 @@ class {{ name }} extends BaseMigration * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Create/TestCreateChange.php b/tests/comparisons/Create/TestCreateChange.php index 071aa889f..ea30b9f1d 100644 --- a/tests/comparisons/Create/TestCreateChange.php +++ b/tests/comparisons/Create/TestCreateChange.php @@ -9,7 +9,7 @@ class TestCreateChange extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * @return void */ public function change(): void diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php index a6dd8bf6a..18acd54fa 100644 --- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php +++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php @@ -9,7 +9,7 @@ class TheDiffAddRemoveMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -41,7 +41,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php index 28fda29b9..1840d737a 100644 --- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php +++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php @@ -9,7 +9,7 @@ class TheDiffAddRemovePgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -33,7 +33,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php index 984829c9c..e82cf01d1 100644 --- a/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php +++ b/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php @@ -9,7 +9,7 @@ class InitialDecimalChangeMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -29,7 +29,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php index a32449e17..c5395b91e 100644 --- a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php +++ b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php @@ -9,7 +9,7 @@ class TheDiffDecimalChangeMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -36,7 +36,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index 679efe87b..2fc3f9f45 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -9,7 +9,7 @@ class TheDiffDefaultMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -139,7 +139,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/default/the_diff_default_pgsql.php b/tests/comparisons/Diff/default/the_diff_default_pgsql.php index 52e1f0766..3ff222c3d 100644 --- a/tests/comparisons/Diff/default/the_diff_default_pgsql.php +++ b/tests/comparisons/Diff/default/the_diff_default_pgsql.php @@ -9,7 +9,7 @@ class TheDiffDefaultPgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -126,7 +126,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php index a8f69ca98..a3836d5a4 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php @@ -9,7 +9,7 @@ class TheDiffSimpleMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -81,7 +81,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php b/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php index 948bcc954..390cfdca9 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php @@ -9,7 +9,7 @@ class TheDiffSimplePgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -62,7 +62,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php index 4e5e45848..e4d325d16 100644 --- a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php @@ -9,7 +9,7 @@ class TheDiffWithAutoIdCompatibleSignedPrimaryKeysMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -25,7 +25,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php index 1a645545d..905a68567 100644 --- a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php @@ -11,7 +11,7 @@ class TheDiffWithAutoIdIncompatibleSignedPrimaryKeysMysql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -35,7 +35,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php index 5d06abc3e..2ca5d4189 100644 --- a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php @@ -11,7 +11,7 @@ class TheDiffWithAutoIdIncompatibleUnsignedPrimaryKeysMysql extends BaseMigratio * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -35,7 +35,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 bbe494204..371b2e35e 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 @@ -11,7 +11,7 @@ class TestSnapshotAutoIdDisabledPgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -409,7 +409,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 8ff44ed60..7d073ad54 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php @@ -9,7 +9,7 @@ class TestSnapshotNotEmptyPgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -349,7 +349,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 25f27019b..1eab3c1d2 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php @@ -9,7 +9,7 @@ class TestSnapshotPluginBlogPgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -349,7 +349,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 2c3cb4f59..0a1835d8c 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php @@ -9,7 +9,7 @@ class TestSnapshotWithChangePgsql extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ 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 12261cdd3..e609f7f39 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 @@ -11,7 +11,7 @@ class TestSnapshotAutoIdDisabledSqlite extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -390,7 +390,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 bef6d8cea..2d5f6bf79 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php @@ -9,7 +9,7 @@ class TestSnapshotNotEmptySqlite extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -330,7 +330,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 f9f840281..2c17be540 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php @@ -9,7 +9,7 @@ class TestSnapshotPluginBlogSqlite extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -330,7 +330,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 857bbde1d..fa3692a80 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php @@ -9,7 +9,7 @@ class TestSnapshotWithChangeSqlite extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ 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 47b630bfd..e7e1eb818 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 @@ -11,7 +11,7 @@ class TestSnapshotAutoIdDisabledSqlserver extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -425,7 +425,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 aa2bc9a34..d5e1c2577 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php @@ -9,7 +9,7 @@ class TestSnapshotNotEmptySqlserver extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -365,7 +365,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 8e49310d4..4d422ca58 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php @@ -9,7 +9,7 @@ class TestSnapshotPluginBlogSqlserver extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -365,7 +365,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 f1282faf4..898c9d92c 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php @@ -9,7 +9,7 @@ class TestSnapshotWithChangeSqlserver extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testAddFieldWithReference.php b/tests/comparisons/Migration/testAddFieldWithReference.php index b6c5ccfb2..4b25ddb91 100644 --- a/tests/comparisons/Migration/testAddFieldWithReference.php +++ b/tests/comparisons/Migration/testAddFieldWithReference.php @@ -9,7 +9,7 @@ class AddCategoryIdToProducts extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreate.php b/tests/comparisons/Migration/testCreate.php index 39bc88392..4483cd1b5 100644 --- a/tests/comparisons/Migration/testCreate.php +++ b/tests/comparisons/Migration/testCreate.php @@ -9,7 +9,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreateDatetime.php b/tests/comparisons/Migration/testCreateDatetime.php index 0547f6851..c4fa3493f 100644 --- a/tests/comparisons/Migration/testCreateDatetime.php +++ b/tests/comparisons/Migration/testCreateDatetime.php @@ -9,7 +9,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreateDropMigration.php b/tests/comparisons/Migration/testCreateDropMigration.php index a7550b664..96526dcfe 100644 --- a/tests/comparisons/Migration/testCreateDropMigration.php +++ b/tests/comparisons/Migration/testCreateDropMigration.php @@ -9,7 +9,7 @@ class DropUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreateFieldLength.php b/tests/comparisons/Migration/testCreateFieldLength.php index 43220d035..7ceee43a2 100644 --- a/tests/comparisons/Migration/testCreateFieldLength.php +++ b/tests/comparisons/Migration/testCreateFieldLength.php @@ -9,7 +9,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreatePrimaryKey.php b/tests/comparisons/Migration/testCreatePrimaryKey.php index 44efb2631..fb36b4a24 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKey.php +++ b/tests/comparisons/Migration/testCreatePrimaryKey.php @@ -11,7 +11,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php index de8c21792..6c7238870 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php +++ b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php @@ -11,7 +11,7 @@ class CreateUsers extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreateWithReferences.php b/tests/comparisons/Migration/testCreateWithReferences.php index 5537e448e..f1abf9f2c 100644 --- a/tests/comparisons/Migration/testCreateWithReferences.php +++ b/tests/comparisons/Migration/testCreateWithReferences.php @@ -9,7 +9,7 @@ class CreatePosts extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php index 6897dd2aa..2e5cdd4ac 100644 --- a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php +++ b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php @@ -9,7 +9,7 @@ class CreateArticles extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/testNoContents.php b/tests/comparisons/Migration/testNoContents.php index 19c2524b5..4fd8ee3fb 100644 --- a/tests/comparisons/Migration/testNoContents.php +++ b/tests/comparisons/Migration/testNoContents.php @@ -9,7 +9,7 @@ class NoContents extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php index c8d07b215..9a066a245 100644 --- a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php +++ b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php @@ -11,7 +11,7 @@ class TestSnapshotAutoIdDisabled extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -415,7 +415,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Migration/test_snapshot_not_empty.php b/tests/comparisons/Migration/test_snapshot_not_empty.php index 4bde415d7..8797de273 100644 --- a/tests/comparisons/Migration/test_snapshot_not_empty.php +++ b/tests/comparisons/Migration/test_snapshot_not_empty.php @@ -9,7 +9,7 @@ class TestSnapshotNotEmpty extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -347,7 +347,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Migration/test_snapshot_plugin_blog.php b/tests/comparisons/Migration/test_snapshot_plugin_blog.php index d33673594..54c449b60 100644 --- a/tests/comparisons/Migration/test_snapshot_plugin_blog.php +++ b/tests/comparisons/Migration/test_snapshot_plugin_blog.php @@ -9,7 +9,7 @@ class TestSnapshotPluginBlog extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -347,7 +347,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 e4f7da4f7..2dbb26532 100644 --- a/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php +++ b/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php @@ -9,7 +9,7 @@ class TestSnapshotPostgresTimestampTzPgsql extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -359,7 +359,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 3da752b24..dd831d8f4 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 @@ -11,7 +11,7 @@ class TestSnapshotWithAutoIdCompatibleSignedPrimaryKeys extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -414,7 +414,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 13f8327da..2d398c11a 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 @@ -11,7 +11,7 @@ class TestSnapshotWithAutoIdIncompatibleSignedPrimaryKeys extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -414,7 +414,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ 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 bdf099943..b0a2f823f 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 @@ -11,7 +11,7 @@ class TestSnapshotWithAutoIdIncompatibleUnsignedPrimaryKeys extends BaseMigratio * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -415,7 +415,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Migration/test_snapshot_with_change.php b/tests/comparisons/Migration/test_snapshot_with_change.php index d4a432337..1fa84a100 100644 --- a/tests/comparisons/Migration/test_snapshot_with_change.php +++ b/tests/comparisons/Migration/test_snapshot_with_change.php @@ -9,7 +9,7 @@ class TestSnapshotWithChange extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ 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 c48d00a4e..14d2e18b9 100644 --- a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php +++ b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php @@ -9,7 +9,7 @@ class TestSnapshotWithNonDefaultCollation extends BaseMigration * Up Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-up-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-up-method * * @return void */ @@ -348,7 +348,7 @@ public function up(): void * Down Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-down-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method * * @return void */ diff --git a/tests/comparisons/Seeds/pgsql/testWithData.php b/tests/comparisons/Seeds/pgsql/testWithData.php index 272d14ecd..1aa40b98a 100644 --- a/tests/comparisons/Seeds/pgsql/testWithData.php +++ b/tests/comparisons/Seeds/pgsql/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php b/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php index 45fda5df6..2b02ec3ba 100644 --- a/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/pgsql/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/php81/testWithData.php b/tests/comparisons/Seeds/php81/testWithData.php index 272d14ecd..1aa40b98a 100644 --- a/tests/comparisons/Seeds/php81/testWithData.php +++ b/tests/comparisons/Seeds/php81/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/php81/testWithDataAndLimit.php b/tests/comparisons/Seeds/php81/testWithDataAndLimit.php index 45fda5df6..2b02ec3ba 100644 --- a/tests/comparisons/Seeds/php81/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/php81/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/sqlserver/testWithData.php b/tests/comparisons/Seeds/sqlserver/testWithData.php index 6cd3d80d6..dc7a7e3c4 100644 --- a/tests/comparisons/Seeds/sqlserver/testWithData.php +++ b/tests/comparisons/Seeds/sqlserver/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php b/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php index 3da226273..d9af7a606 100644 --- a/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/sqlserver/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testBasicBaking.php b/tests/comparisons/Seeds/testBasicBaking.php index ce5dc9915..2aa8dd0b2 100644 --- a/tests/comparisons/Seeds/testBasicBaking.php +++ b/tests/comparisons/Seeds/testBasicBaking.php @@ -14,7 +14,7 @@ class ArticlesSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testPrettifyArray.php b/tests/comparisons/Seeds/testPrettifyArray.php index c5a2978da..e077a82d0 100644 --- a/tests/comparisons/Seeds/testPrettifyArray.php +++ b/tests/comparisons/Seeds/testPrettifyArray.php @@ -14,7 +14,7 @@ class TextsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testWithData.php b/tests/comparisons/Seeds/testWithData.php index 6cd3d80d6..dc7a7e3c4 100644 --- a/tests/comparisons/Seeds/testWithData.php +++ b/tests/comparisons/Seeds/testWithData.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testWithDataAndFields.php b/tests/comparisons/Seeds/testWithDataAndFields.php index 02bdace54..7249f5a82 100644 --- a/tests/comparisons/Seeds/testWithDataAndFields.php +++ b/tests/comparisons/Seeds/testWithDataAndFields.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/comparisons/Seeds/testWithDataAndLimit.php b/tests/comparisons/Seeds/testWithDataAndLimit.php index 3da226273..d9af7a606 100644 --- a/tests/comparisons/Seeds/testWithDataAndLimit.php +++ b/tests/comparisons/Seeds/testWithDataAndLimit.php @@ -14,7 +14,7 @@ class EventsSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ diff --git a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php index 952893f7b..5b1e7cedd 100644 --- a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php +++ b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginLettersSeed.php @@ -13,7 +13,7 @@ class PluginLettersSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php index 1a3f77c39..9df45af15 100644 --- a/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php +++ b/tests/test_app/Plugin/TestBlog/config/CallSeeds/PluginSubLettersSeed.php @@ -13,7 +13,7 @@ class PluginSubLettersSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php b/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php index efa478b60..8f8f4b53a 100644 --- a/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php +++ b/tests/test_app/config/AltSeeds/AnotherNumbersSeed.php @@ -13,7 +13,7 @@ class AnotherNumbersSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/AltSeeds/NumbersAltSeed.php b/tests/test_app/config/AltSeeds/NumbersAltSeed.php index 1455134eb..3d640f482 100644 --- a/tests/test_app/config/AltSeeds/NumbersAltSeed.php +++ b/tests/test_app/config/AltSeeds/NumbersAltSeed.php @@ -13,7 +13,7 @@ class NumbersAltSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php b/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php index 1543dd13e..9f0ca6dac 100644 --- a/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php +++ b/tests/test_app/config/AnonymousMigrations/2024_12_08_150000_CreateTestAnonymousTable.php @@ -9,7 +9,7 @@ * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method */ public function change(): void { diff --git a/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php index 5558fb593..029f48b9a 100644 --- a/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php +++ b/tests/test_app/config/BaseSeeds/MigrationSeedNumbers.php @@ -13,7 +13,7 @@ class MigrationSeedNumbers extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/CallSeeds/DatabaseSeed.php b/tests/test_app/config/CallSeeds/DatabaseSeed.php index e8e69f01d..8321e103d 100644 --- a/tests/test_app/config/CallSeeds/DatabaseSeed.php +++ b/tests/test_app/config/CallSeeds/DatabaseSeed.php @@ -13,7 +13,7 @@ class DatabaseSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/CallSeeds/LettersSeed.php b/tests/test_app/config/CallSeeds/LettersSeed.php index a2591b6ff..c4a8267c9 100644 --- a/tests/test_app/config/CallSeeds/LettersSeed.php +++ b/tests/test_app/config/CallSeeds/LettersSeed.php @@ -13,7 +13,7 @@ class LettersSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/CallSeeds/NumbersCallSeed.php b/tests/test_app/config/CallSeeds/NumbersCallSeed.php index 24f56bde2..3f36c1b5c 100644 --- a/tests/test_app/config/CallSeeds/NumbersCallSeed.php +++ b/tests/test_app/config/CallSeeds/NumbersCallSeed.php @@ -13,7 +13,7 @@ class NumbersCallSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/CallSeeds/ShortNameCallSeed.php b/tests/test_app/config/CallSeeds/ShortNameCallSeed.php index 00ff336e3..ad9485c29 100644 --- a/tests/test_app/config/CallSeeds/ShortNameCallSeed.php +++ b/tests/test_app/config/CallSeeds/ShortNameCallSeed.php @@ -13,7 +13,7 @@ class ShortNameCallSeed extends BaseSeed * Write your database seeder using this method. * * More information on writing seeders is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html */ public function run(): void { diff --git a/tests/test_app/config/MigrationsDiffDefault/20160128183623_AlterArticlesDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160128183623_AlterArticlesDefault.php index ca59807bc..2f9549202 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160128183623_AlterArticlesDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160128183623_AlterArticlesDefault.php @@ -8,7 +8,7 @@ class AlterArticlesDefault extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/test_app/config/MigrationsDiffDefault/20160128183652_AlterArticlesSlugDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160128183652_AlterArticlesSlugDefault.php index db70be01e..39918b70a 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160128183652_AlterArticlesSlugDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160128183652_AlterArticlesSlugDefault.php @@ -8,7 +8,7 @@ class AlterArticlesSlugDefault extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php index f1e3b5685..beb38932c 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160128183952_CreateUsersDefault.php @@ -8,7 +8,7 @@ class CreateUsersDefault extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/test_app/config/MigrationsDiffDefault/20160128184109_AlterArticlesFkDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160128184109_AlterArticlesFkDefault.php index a66b5645b..46c704a1e 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160128184109_AlterArticlesFkDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160128184109_AlterArticlesFkDefault.php @@ -8,7 +8,7 @@ class AlterArticlesFkDefault extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php b/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php index e4776347a..2de701371 100644 --- a/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php +++ b/tests/test_app/config/MigrationsDiffDefault/20160414193900_CreateTagsDefault.php @@ -8,7 +8,7 @@ class CreateTagsDefault extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/test_app/config/MigrationsDiffSimple/20160128183623_AlterArticlesSimple.php b/tests/test_app/config/MigrationsDiffSimple/20160128183623_AlterArticlesSimple.php index df5ad722b..0a8b3cccc 100644 --- a/tests/test_app/config/MigrationsDiffSimple/20160128183623_AlterArticlesSimple.php +++ b/tests/test_app/config/MigrationsDiffSimple/20160128183623_AlterArticlesSimple.php @@ -8,7 +8,7 @@ class AlterArticlesSimple extends BaseMigration * Change Method. * * More information on this method is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html#the-change-method + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-change-method * * @return void */ diff --git a/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php b/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php index 5af15e185..6c9f3ce63 100644 --- a/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php +++ b/tests/test_app/config/Reversiblemigrations/20180516025208_create_test_index_limit_specifier_table.php @@ -11,7 +11,7 @@ class CreateTestIndexLimitSpecifierTable extends BaseMigration * Write your reversible migrations using this method. * * More information on writing migrations is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html * * The following commands can be used in this method and Migrations will * automatically reverse them when rolling back: diff --git a/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php b/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php index 45f3dad78..99f47ef19 100644 --- a/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php +++ b/tests/test_app/config/Reversiblemigrations/20190928220334_add_column_index_fk.php @@ -12,7 +12,7 @@ class AddColumnIndexFk extends BaseMigration * Write your reversible migrations using this method. * * More information on writing migrations is available here: - * https://book.cakephp.org/migrations/5/en/migrations.html + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html * * The following commands can be used in this method and Migrations will * automatically reverse them when rolling back: diff --git a/tests/test_app/config/Seeds/AnonymousStoreSeed.php b/tests/test_app/config/Seeds/AnonymousStoreSeed.php index 4812a5b66..293061f09 100644 --- a/tests/test_app/config/Seeds/AnonymousStoreSeed.php +++ b/tests/test_app/config/Seeds/AnonymousStoreSeed.php @@ -11,7 +11,7 @@ * Write your database seeder using this method. * * More information on writing seeds is available here: - * https://book.cakephp.org/migrations/5/en/seeding.html + * https://book.cakephp.org/migrations/5/guides/seeding.html * * @return void */ From 30698f810c6c72bbac3dee9afd493a91de61175c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 22 May 2026 21:34:09 +0200 Subject: [PATCH 50/72] Quote constraint name in MySQL DROP FOREIGN KEY statement (#1088) The MySQL adapter built `DROP FOREIGN KEY ` without quoting the constraint identifier. That produces invalid SQL whenever the constraint name is not a bare identifier, e.g. names containing whitespace, or the numeric names ("1", "2", ...) that MariaDB 12 auto-assigns to foreign keys created without an explicit name. Quote the identifier via quoteColumnName(), matching the Postgres and SQL Server adapters, which already quote the dropped constraint name. --- src/Db/Adapter/MysqlAdapter.php | 2 +- .../TestCase/Db/Adapter/MysqlAdapterTest.php | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index f98148f76..3ebceb9e1 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -1061,7 +1061,7 @@ protected function getDropForeignKeyInstructions(string $tableName, string $cons { $alter = sprintf( 'DROP FOREIGN KEY %s', - $constraint, + $this->quoteColumnName($constraint), ); return new AlterInstructions([$alter]); diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 25649d501..7d26ca95b 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -1848,6 +1848,32 @@ public function testDropForeignKeyWithIdenticalMultipleColumns(): void $this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'ref_table_fk_2')); } + public function testDropForeignKeyWithNameContainingWhitespace(): void + { + $refTable = new Table('ref_table', [], $this->adapter); + $refTable->addColumn('field1', 'string')->save(); + + $table = new Table('table', [], $this->adapter); + $key = (new ForeignKey()) + ->setName('ref table fk') + ->setColumns(['ref_table_id']) + ->setReferencedTable('ref_table') + ->setReferencedColumns(['id']); + $table + ->addColumn('ref_table_id', 'integer', ['signed' => false]) + ->addForeignKey($key) + ->save(); + + $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [], 'ref table fk')); + + // The constraint name must be quoted in the DROP statement, otherwise + // names with whitespace (or numeric names auto-assigned by MariaDB 12) + // produce invalid SQL. + $this->adapter->dropForeignKey($table->getName(), [], 'ref table fk'); + + $this->assertFalse($this->adapter->hasForeignKey($table->getName(), [], 'ref table fk')); + } + public static function nonExistentForeignKeyColumnsProvider(): array { return [ From 176df9933fa8ea72e54e4130ededd16cd971098d Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sat, 23 May 2026 13:24:19 +0200 Subject: [PATCH 51/72] update stan (#1089) * update stan * fix rector issues --- .phive/phars.xml | 2 +- src/Command/BakeMigrationCommand.php | 2 +- tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.phive/phars.xml b/.phive/phars.xml index 4d447d1a0..848f478f2 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,4 +1,4 @@ - + diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index 66fb0580e..ce9af3cb2 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -245,7 +245,7 @@ public function getOptionParser(): ConsoleOptionParser /** * @inheritDoc */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser = parent::buildOptionParser($parser); diff --git a/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php b/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php index e7bc5ae24..c52597f6c 100644 --- a/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php +++ b/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php @@ -23,7 +23,7 @@ public static function defaultName(): string /** * @inheritDoc */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { return parent::buildOptionParser($parser) ->addOption('test-target-folder') From dd5f01a02bc9a5345fbb22509dcf6d6c0f976db0 Mon Sep 17 00:00:00 2001 From: Jamison Bryant Date: Wed, 27 May 2026 21:53:59 -0400 Subject: [PATCH 52/72] Add security policy --- .github/SECURITY.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/SECURITY.md diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 000000000..6d7de0058 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,29 @@ +# Security Policy + +## Supported Versions + +We support fixing security issues on the following releases: + +| Version | Supported | Security fixes until +| ------- | ------------------ | -------------------- +| 5.x | :white_check_mark: | Currently supported +| 4.x | :white_check_mark: | The release of 6.x +| 3.x | :white_check_mark: | 36 Months after the release of CakePHP 5.0 (09 Sep 2026) +| 2.x | :x: | No longer supported + +## Reporting a Vulnerability + +If you've found a security issue in CakePHP Migrations, please use the following procedure +instead of the normal bug reporting system. Instead of using the bug tracker, +or one of the support forums please send an email to security [at] cakephp.org. Emails +sent to this address go to the CakePHP core team on a private mailing list. + +For each report, we try to first confirm the vulnerability. Once confirmed, +the CakePHP team will take the following actions: + +* Acknowledge to the reporter that we've received the issue, and are + working on a fix. We ask that the reporter keep the issue confidential until we announce it. +* Get a fix/patch prepared. +* Prepare a post describing the vulnerability, and the possible exploits. +* Release new versions of all affected versions. +* Prominently feature the problem in the release announcement From 3978dcaac232c950ed21a3e735ef67aa564b2436 Mon Sep 17 00:00:00 2001 From: Jamison Bryant Date: Thu, 28 May 2026 09:24:50 -0400 Subject: [PATCH 53/72] Reference wiki version map instead of concrete table Avoids duplicating version support info that is already maintained in the wiki, reducing maintenance burden. --- .github/SECURITY.md | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 6d7de0058..ec319b782 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,14 +2,9 @@ ## Supported Versions -We support fixing security issues on the following releases: - -| Version | Supported | Security fixes until -| ------- | ------------------ | -------------------- -| 5.x | :white_check_mark: | Currently supported -| 4.x | :white_check_mark: | The release of 6.x -| 3.x | :white_check_mark: | 36 Months after the release of CakePHP 5.0 (09 Sep 2026) -| 2.x | :x: | No longer supported +Security fixes are applied to all active versions listed in the +[version map](https://github.com/cakephp/migrations/wiki#version-map). +Versions marked as EOL no longer receive fixes. ## Reporting a Vulnerability From 5c60b25a8c42ad3ee4a10fd3b994dff8c681b134 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:19:37 -0400 Subject: [PATCH 54/72] Bump codecov/codecov-action from 6 to 7 (#1091) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6 to 7. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v6...v7) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45fa76875..02ac5b287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,7 @@ jobs: - name: Code Coverage Report if: success() && matrix.php-version == '8.1' && matrix.db-type == 'mysql' - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 testsuite-windows: runs-on: windows-2022 @@ -240,7 +240,7 @@ jobs: vendor/bin/phpunit --coverage-clover=coverage.xml - name: Submit code coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 cs-stan: uses: cakephp/.github/.github/workflows/cs-stan.yml@5.x From 7fc6cc14d9af00520a70999613155166ae688767 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:03:40 +0200 Subject: [PATCH 55/72] Bump actions/checkout from 6 to 7 (#1093) Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- .github/workflows/deploy_docs_3x.yml | 2 +- .github/workflows/deploy_docs_4x.yml | 2 +- .github/workflows/deploy_docs_5x.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02ac5b287..ba454ebd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false @@ -177,7 +177,7 @@ jobs: PHP_VERSION: '8.2' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: persist-credentials: false diff --git a/.github/workflows/deploy_docs_3x.yml b/.github/workflows/deploy_docs_3x.yml index 1bfda3583..a22bdc7ee 100644 --- a/.github/workflows/deploy_docs_3x.yml +++ b/.github/workflows/deploy_docs_3x.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cloning repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/deploy_docs_4x.yml b/.github/workflows/deploy_docs_4x.yml index 8f04a50db..af3f7f76a 100644 --- a/.github/workflows/deploy_docs_4x.yml +++ b/.github/workflows/deploy_docs_4x.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cloning repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 diff --git a/.github/workflows/deploy_docs_5x.yml b/.github/workflows/deploy_docs_5x.yml index 2f5c1a27e..4d186664c 100644 --- a/.github/workflows/deploy_docs_5x.yml +++ b/.github/workflows/deploy_docs_5x.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cloning repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 From a3ce85be305440e727da955bde741f5aaab56799 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:41:03 +0200 Subject: [PATCH 56/72] Bump actions/cache from 5 to 6 (#1092) Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba454ebd4..b1ca9ed4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: run: echo "date=$(date +'%Y-%m')" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} @@ -195,7 +195,7 @@ jobs: key: ${{ steps.key-date.outputs.date }} - name: Cache PHP extensions - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.php-ext-cache.outputs.dir }} key: ${{ runner.os }}-php-ext-${{ steps.php-ext-cache.outputs.key }} @@ -223,7 +223,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} From 400608e48408da9a8c8e3b6089422582943ccae9 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Wed, 1 Jul 2026 07:58:37 +0200 Subject: [PATCH 57/72] npm update for docs --- docs/package-lock.json | 771 ++++++++++++++++++++++------------------- 1 file changed, 405 insertions(+), 366 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index c65b0ab07..eab6ff358 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -14,30 +14,30 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -47,13 +47,13 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -70,27 +70,27 @@ } }, "node_modules/@docsearch/css": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.2.tgz", - "integrity": "sha512-fH/cn8BjEEdM2nJdjNMHIvOVYupG6AIDtFVDgIZrNzdCSj4KXr9kd+hsehqsNGYjpUjObeKYKvgy/IwCb1jZYQ==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.3.tgz", + "integrity": "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==", "license": "MIT" }, "node_modules/@docsearch/js": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.2.tgz", - "integrity": "sha512-qj1yoxl3y4GKoK7+VM6fq/rQqPnvUmg3IKzJ9x0VzN14QVzdB/SG/J6VfV1BWT5RcPUFxIcVwoY1fwHM2fSRRw==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.3.tgz", + "integrity": "sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ==", "license": "MIT" }, "node_modules/@docsearch/sidepanel-js": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.2.tgz", - "integrity": "sha512-Pni85AP/GwRj7fFg8cBJp0U04tzbueBvWSd3gysgnOsVnQVSZwSYncfErUScLE1CAtR+qocPDFjmYR9AMRNJtQ==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.3.tgz", + "integrity": "sha512-grGSmvXzG0if+mrzdIKykvpIAuEQ9u0sEJ2eLRRCaQfJvsWqh2C2/aY04bIzWvDh7myi5rvl8D+tUNsVrjYQ3A==", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -120,9 +120,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -136,9 +136,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -152,9 +152,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -168,9 +168,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -200,9 +200,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -216,9 +216,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -232,9 +232,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -248,9 +248,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -264,9 +264,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -280,9 +280,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -312,9 +312,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -328,9 +328,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -344,9 +344,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -360,9 +360,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -376,9 +376,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -392,9 +392,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -408,9 +408,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -424,9 +424,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -440,9 +440,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -472,9 +472,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -488,9 +488,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -504,9 +504,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.76", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.76.tgz", - "integrity": "sha512-lLRlA8yaf+1L5VCPRvR9lynoSklsddKHEylchmZJKdj/q2xVQ1ZAEJ8SCQlv9cbgtMefnlyM98U+8Si2aoFZPA==", + "version": "1.2.88", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.88.tgz", + "integrity": "sha512-+cvi1qCuvReL29ehi6t62L4fb7GDXe+UlGHFcsJcV7I2l9wtqn9XE2IBKcDr3CI5iGUGS5ISnXv699pSGpyx1Q==", "license": "CC0-1.0", "dependencies": { "@iconify/types": "*" @@ -525,15 +525,15 @@ "license": "MIT" }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", - "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", - "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -544,9 +544,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", - "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -557,9 +557,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", - "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -570,9 +570,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", - "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -583,9 +583,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", - "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -596,9 +596,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", - "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -609,12 +609,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", - "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -622,12 +625,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", - "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -635,12 +641,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", - "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -648,12 +657,15 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", - "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -661,12 +673,15 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", - "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -674,12 +689,15 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -687,12 +705,15 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", - "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -700,12 +721,15 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -713,12 +737,15 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", - "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -726,12 +753,15 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", - "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -739,12 +769,15 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", - "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -752,12 +785,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", - "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -765,12 +801,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", - "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -778,9 +817,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", - "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -791,9 +830,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", - "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -804,9 +843,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", - "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -817,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", - "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -830,9 +869,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", - "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -843,9 +882,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", - "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -933,9 +972,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/hast": { @@ -991,18 +1030,18 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", - "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.2" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1013,141 +1052,141 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", - "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/shared": "3.5.32", + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", - "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", - "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.2", - "@vue/compiler-core": "3.5.32", - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32", + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.8", + "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", - "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" } }, "node_modules/@vue/devtools-api": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz", - "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz", + "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.1.1" + "@vue/devtools-kit": "^8.1.5" } }, "node_modules/@vue/devtools-kit": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz", - "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz", + "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.1.1", + "@vue/devtools-shared": "^8.1.5", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "node_modules/@vue/devtools-shared": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz", - "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz", + "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==", "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", - "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.32" + "@vue/shared": "3.5.39" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", - "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", - "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.32", - "@vue/runtime-core": "3.5.32", - "@vue/shared": "3.5.32", + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", - "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" }, "peerDependencies": { - "vue": "3.5.32" + "vue": "3.5.39" } }, "node_modules/@vue/shared": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", - "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz", - "integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.2.1", - "@vueuse/shared": "14.2.1" + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1157,13 +1196,13 @@ } }, "node_modules/@vueuse/integrations": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.2.1.tgz", - "integrity": "sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.3.0.tgz", + "integrity": "sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==", "license": "MIT", "dependencies": { - "@vueuse/core": "14.2.1", - "@vueuse/shared": "14.2.1" + "@vueuse/core": "14.3.0", + "@vueuse/shared": "14.3.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1223,18 +1262,18 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz", - "integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "14.2.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz", - "integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -1333,9 +1372,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1345,32 +1384,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/estree-walker": { @@ -1397,12 +1436,12 @@ } }, "node_modules/focus-trap": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.0.1.tgz", - "integrity": "sha512-9ptSG6z51YQOstI/oN4XuVGP/03u2nh0g//qz7L6zX0i6PZiPnkcf3GenXq7N2hZnASXaMxTPpbKwdI+PFvxlw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.2.tgz", + "integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==", "license": "MIT", "dependencies": { - "tabbable": "^6.4.0" + "tabbable": "^6.5.0" } }, "node_modules/fsevents": { @@ -1603,9 +1642,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "funding": [ { "type": "github", @@ -1621,18 +1660,18 @@ } }, "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "license": "MIT" }, "node_modules/oniguruma-to-es": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", - "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { - "oniguruma-parser": "^0.12.1", + "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } @@ -1662,9 +1701,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "funding": [ { "type": "opencollective", @@ -1681,7 +1720,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1690,9 +1729,9 @@ } }, "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "license": "MIT", "funding": { "type": "github", @@ -1724,12 +1763,12 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", - "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -1739,31 +1778,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.1", - "@rollup/rollup-android-arm64": "4.60.1", - "@rollup/rollup-darwin-arm64": "4.60.1", - "@rollup/rollup-darwin-x64": "4.60.1", - "@rollup/rollup-freebsd-arm64": "4.60.1", - "@rollup/rollup-freebsd-x64": "4.60.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", - "@rollup/rollup-linux-arm-musleabihf": "4.60.1", - "@rollup/rollup-linux-arm64-gnu": "4.60.1", - "@rollup/rollup-linux-arm64-musl": "4.60.1", - "@rollup/rollup-linux-loong64-gnu": "4.60.1", - "@rollup/rollup-linux-loong64-musl": "4.60.1", - "@rollup/rollup-linux-ppc64-gnu": "4.60.1", - "@rollup/rollup-linux-ppc64-musl": "4.60.1", - "@rollup/rollup-linux-riscv64-gnu": "4.60.1", - "@rollup/rollup-linux-riscv64-musl": "4.60.1", - "@rollup/rollup-linux-s390x-gnu": "4.60.1", - "@rollup/rollup-linux-x64-gnu": "4.60.1", - "@rollup/rollup-linux-x64-musl": "4.60.1", - "@rollup/rollup-openbsd-x64": "4.60.1", - "@rollup/rollup-openharmony-arm64": "4.60.1", - "@rollup/rollup-win32-arm64-msvc": "4.60.1", - "@rollup/rollup-win32-ia32-msvc": "4.60.1", - "@rollup/rollup-win32-x64-gnu": "4.60.1", - "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -1817,19 +1856,19 @@ } }, "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -1945,12 +1984,12 @@ } }, "node_modules/vite": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", - "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -2065,16 +2104,16 @@ } }, "node_modules/vue": { - "version": "3.5.32", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", - "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.32", - "@vue/compiler-sfc": "3.5.32", - "@vue/runtime-dom": "3.5.32", - "@vue/server-renderer": "3.5.32", - "@vue/shared": "3.5.32" + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" }, "peerDependencies": { "typescript": "*" From 46bab410d0b8c08b5211813b994f620540738a2d Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 3 Jul 2026 09:54:05 +0200 Subject: [PATCH 58/72] Do not auto-increment composite primary key columns in bake migration (#1095) Baking a migration with multiple integer primary key columns, e.g. a join table: bin/cake bake migration CreateArticlesTags article_id:integer:primary tag_id:integer:primary generated `autoIncrement => true` on every integer primary key column. MySQL then rejects the table: SQLSTATE[42000]: Syntax error or access violation: 1075 Incorrect table definition; there can be only one auto column and it must be defined as a key By convention only a single integer primary key auto-increments; a composite primary key (typical for join tables) must not. ColumnParser now only emits autoIncrement when there is exactly one primary key column. --- src/Util/ColumnParser.php | 13 +++++++++ tests/TestCase/Util/ColumnParserTest.php | 34 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/Util/ColumnParser.php b/src/Util/ColumnParser.php index 54c45151e..fbfbafdca 100644 --- a/src/Util/ColumnParser.php +++ b/src/Util/ColumnParser.php @@ -46,6 +46,7 @@ class ColumnParser public function parseFields(array $arguments): array { $fields = []; + $primaryKeys = []; $arguments = $this->validArguments($arguments); foreach ($arguments as $field) { preg_match($this->regexpParseColumn, $field, $matches); @@ -90,11 +91,23 @@ public function parseFields(array $arguments): array } } + if ($isPrimaryKey) { + $primaryKeys[] = $field; + } + if ($isPrimaryKey && $type === 'integer') { $fields[$field]['options']['autoIncrement'] = true; } } + // A composite primary key (e.g. a join table) must not auto-increment. + // Only a single integer primary key column gets auto-increment. + if (count($primaryKeys) > 1) { + foreach ($primaryKeys as $field) { + unset($fields[$field]['options']['autoIncrement']); + } + } + return $fields; } diff --git a/tests/TestCase/Util/ColumnParserTest.php b/tests/TestCase/Util/ColumnParserTest.php index 5fddc4799..5e332e32e 100644 --- a/tests/TestCase/Util/ColumnParserTest.php +++ b/tests/TestCase/Util/ColumnParserTest.php @@ -227,6 +227,40 @@ public function testParseFields(): void $this->assertEquals($expected, $actual); } + /** + * A composite primary key (e.g. a join table) must not auto-increment its + * integer columns, otherwise MySQL rejects the table with + * "there can be only one auto column". + * + * @return void + */ + public function testParseFieldsCompositePrimaryKey(): void + { + $expected = [ + 'article_id' => [ + 'columnType' => 'integer', + 'options' => [ + 'null' => false, + 'default' => null, + 'limit' => 11, + ], + ], + 'tag_id' => [ + 'columnType' => 'integer', + 'options' => [ + 'null' => false, + 'default' => null, + 'limit' => 11, + ], + ], + ]; + $actual = $this->columnParser->parseFields([ + 'article_id:integer:primary', + 'tag_id:integer:primary', + ]); + $this->assertEquals($expected, $actual); + } + public function testParseIndexes(): void { $this->assertEquals(['UNIQUE_ID' => [ From ab5e25e185b751db9c19269c55036063d216f1cd Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sun, 5 Jul 2026 18:39:17 +0200 Subject: [PATCH 59/72] Auto-generate check constraint name when name is empty (#1098) `AddCheckConstraint::build()` defaults a missing name to an empty string, but the adapters only auto-generated a name when it was `null`. Since `Cake\Database\Schema\Constraint` types the name as a non-nullable string, `getName()` never returns `null`, so the auto-generation branch was dead code and an unnamed check constraint produced invalid SQL, e.g. `ADD CONSTRAINT CHECK (...)` which PostgreSQL rejects with `SQLSTATE[42601]: Syntax error at or near "CHECK"`. Treat an empty string the same as `null` in the Postgres, MySQL and SQLite adapters so a name is generated. Add regression tests for the empty-name path and tighten the MySQL/MariaDB expectation now that migrations supplies the generated name explicitly instead of relying on the database fallback. --- src/Db/Adapter/MysqlAdapter.php | 2 +- src/Db/Adapter/PostgresAdapter.php | 2 +- src/Db/Adapter/SqliteAdapter.php | 2 +- tests/TestCase/Db/Adapter/MysqlAdapterTest.php | 6 ++++-- tests/TestCase/Db/Adapter/PostgresAdapterTest.php | 15 +++++++++++++++ tests/TestCase/Db/Adapter/SqliteAdapterTest.php | 15 +++++++++++++++ 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Db/Adapter/MysqlAdapter.php b/src/Db/Adapter/MysqlAdapter.php index 3ebceb9e1..b3f0002bf 100644 --- a/src/Db/Adapter/MysqlAdapter.php +++ b/src/Db/Adapter/MysqlAdapter.php @@ -1121,7 +1121,7 @@ protected function getCheckConstraints(string $tableName): array protected function getAddCheckConstraintInstructions(TableMetadata $table, CheckConstraint $checkConstraint): AlterInstructions { $constraintName = $checkConstraint->getName(); - if ($constraintName === null) { + if ($constraintName === null || $constraintName === '') { // Auto-generate constraint name if not provided $constraintName = $table->getName() . '_chk_' . substr(md5($checkConstraint->getExpression()), 0, 8); } diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 89fc010b1..84c3f1803 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -826,7 +826,7 @@ protected function getCheckConstraints(string $tableName): array protected function getAddCheckConstraintInstructions(TableMetadata $table, CheckConstraint $checkConstraint): AlterInstructions { $constraintName = $checkConstraint->getName(); - if ($constraintName === null) { + if ($constraintName === null || $constraintName === '') { // Auto-generate constraint name if not provided $parts = $this->getSchemaName($table->getName()); $constraintName = $parts['table'] . '_chk_' . substr(md5($checkConstraint->getExpression()), 0, 8); diff --git a/src/Db/Adapter/SqliteAdapter.php b/src/Db/Adapter/SqliteAdapter.php index 853c7ae46..9736f38f4 100644 --- a/src/Db/Adapter/SqliteAdapter.php +++ b/src/Db/Adapter/SqliteAdapter.php @@ -1546,7 +1546,7 @@ protected function getAddCheckConstraintInstructions(TableMetadata $table, Check $instructions->addPostStep(function (array $state) use ($checkConstraint): array { $constraintName = $checkConstraint->getName(); - if ($constraintName === null) { + if ($constraintName === null || $constraintName === '') { // Auto-generate constraint name if not provided $constraintName = 'chk_' . substr(md5($checkConstraint->getExpression()), 0, 8); } diff --git a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php index 7d26ca95b..df46e1ec0 100644 --- a/tests/TestCase/Db/Adapter/MysqlAdapterTest.php +++ b/tests/TestCase/Db/Adapter/MysqlAdapterTest.php @@ -2677,8 +2677,10 @@ public function testAddCheckConstraintWithAutoGeneratedName(): void $dialect = $driver->schemaDialect(); $constraints = $dialect->describeCheckConstraints('check_table2'); $this->assertCount(1, $constraints); - $expected = $driver->isMariaDb() ? 'CONSTRAINT_1' : 'check_table2_chk_'; - $this->assertStringContainsString($expected, $constraints[0]['name']); + // The name is generated by migrations (not left to the DB), so it is + // identical on MySQL and MariaDB instead of the DB fallback CONSTRAINT_1. + $expectedName = 'check_table2_chk_' . substr(md5('age >= 18'), 0, 8); + $this->assertSame($expectedName, $constraints[0]['name']); } public function testHasCheckConstraint(): void diff --git a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php index e6a4e3eaa..831386949 100644 --- a/tests/TestCase/Db/Adapter/PostgresAdapterTest.php +++ b/tests/TestCase/Db/Adapter/PostgresAdapterTest.php @@ -2950,6 +2950,21 @@ public function testAddCheckConstraint(): void $this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive')); } + public function testAddCheckConstraintAutoGeneratesName(): void + { + $table = new Table('check_table_auto', [], $this->adapter); + $table->addColumn('quantity', 'integer') + ->create(); + + $expression = 'quantity >= 0'; + // An empty name must trigger auto-generation, not emit invalid `ADD CONSTRAINT CHECK`. + $checkConstraint = new CheckConstraint('', $expression); + $this->adapter->addCheckConstraint($table->getTable(), $checkConstraint); + + $expectedName = 'check_table_auto_chk_' . substr(md5($expression), 0, 8); + $this->assertTrue($this->adapter->hasCheckConstraint('check_table_auto', $expectedName)); + } + public function testHasCheckConstraint(): void { $table = new Table('check_table3', [], $this->adapter); diff --git a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php index 9eaa98db0..05438697b 100644 --- a/tests/TestCase/Db/Adapter/SqliteAdapterTest.php +++ b/tests/TestCase/Db/Adapter/SqliteAdapterTest.php @@ -3173,6 +3173,21 @@ public function testAddCheckConstraint(): void $this->assertTrue($this->adapter->hasCheckConstraint('check_table', 'price_positive')); } + public function testAddCheckConstraintAutoGeneratesName(): void + { + $table = new Table('check_table_auto', [], $this->adapter); + $table->addColumn('quantity', 'integer') + ->create(); + + $expression = 'quantity >= 0'; + // An empty name must trigger auto-generation, not produce an unnamed constraint. + $checkConstraint = new CheckConstraint('', $expression); + $this->adapter->addCheckConstraint($table->getTable(), $checkConstraint); + + $expectedName = 'chk_' . substr(md5($expression), 0, 8); + $this->assertTrue($this->adapter->hasCheckConstraint('check_table_auto', $expectedName)); + } + public function testHasCheckConstraint(): void { $table = new Table('check_table3', [], $this->adapter); From f08f5bc172540fe99f30e9465addeb384c52230a Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 9 Jul 2026 05:05:44 +0200 Subject: [PATCH 60/72] Make the database adapter available inside shouldExecute() (#1100) The adapter was only set on the migration/seed by the Environment, right before running the migration body - after the shouldExecute() gate in the Manager. As a result shouldExecute() could not query the database (calling getAdapter() threw "Adapter not set."), so eligibility could only depend on local application state, not the state of the database. Set the environment's adapter on the migration and seed before the shouldExecute() check. getEnvironment()->getAdapter() is memoized, so this adds no extra connection, and the Environment still (re)sets and wraps the adapter before executing the body, leaving migrate/rollback behavior unchanged. --- .../writing-migrations/migration-methods.md | 23 +++++++++++++++++++ src/Migration/Manager.php | 7 ++++++ tests/TestCase/Migration/ManagerTest.php | 16 +++++++++++++ ...05100_should_execute_with_db_migration.php | 20 ++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php diff --git a/docs/en/guides/writing-migrations/migration-methods.md b/docs/en/guides/writing-migrations/migration-methods.md index 2ac07c995..b72cad028 100644 --- a/docs/en/guides/writing-migrations/migration-methods.md +++ b/docs/en/guides/writing-migrations/migration-methods.md @@ -101,6 +101,29 @@ migration. This can be used to prevent the migration from being executed at this time. It always returns `true` by default. You can override it in your custom `BaseMigration` implementation. +The database adapter is available inside `shouldExecute()`, so the decision can +be based on the current state of the database (for example the server version or +whether a table already exists): + +```php +hasTable('some_table'); + } + + public function change(): void + { + // ... + } +} +``` + ## Working With Tables The Table object enables you to manipulate database tables using PHP code. You diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 8b011c373..311085d06 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -509,6 +509,10 @@ public function executeMigration(MigrationInterface $migration, string $directio { $this->getIo()->out(''); + // Make the adapter available so shouldExecute() can inspect the database. + // The environment sets it again (and wraps it for down/change) before running the migration body. + $migration->setAdapter($this->getEnvironment()->getAdapter()); + // Skip the migration if it should not be executed if (!$migration->shouldExecute()) { $this->printMigrationStatus($migration, 'skipped'); @@ -540,6 +544,9 @@ public function executeMigration(MigrationInterface $migration, string $directio */ public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake = false): void { + // Make the adapter available so shouldExecute() can inspect the database. + $seed->setAdapter($this->getEnvironment()->getAdapter()); + // Skip the seed if it should not be executed if (!$seed->shouldExecute()) { $this->getIo()->out(''); diff --git a/tests/TestCase/Migration/ManagerTest.php b/tests/TestCase/Migration/ManagerTest.php index 3d7ce3d44..8d88b5755 100644 --- a/tests/TestCase/Migration/ManagerTest.php +++ b/tests/TestCase/Migration/ManagerTest.php @@ -2987,4 +2987,20 @@ public function testMigrationWillNotBeExecuted(): void $this->assertTrue($adapter->hasTable('info')); } + + public function testMigrationShouldExecuteCanQueryDatabase(): void + { + $configArray = $this->getConfigArray(); + $adapter = $this->manager->getEnvironment()->getAdapter(); + + // override the migrations directory to use a shouldExecute() that queries the database + $configArray['paths']['migrations'] = ROOT . '/config/ShouldExecuteWithDb/'; + $config = new Config($configArray); + + // shouldExecute() calls getAdapter()->hasTable(); before the fix this threw 'Adapter not set.' + $this->manager->setConfig($config); + $this->manager->migrate(20201207205100); + + $this->assertTrue($adapter->hasTable('info')); + } } diff --git a/tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php b/tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php new file mode 100644 index 000000000..0f158bda6 --- /dev/null +++ b/tests/test_app/config/ShouldExecuteWithDb/20201207205100_should_execute_with_db_migration.php @@ -0,0 +1,20 @@ +table('info')->create(); + } + + public function shouldExecute(): bool + { + // Query the database to decide eligibility. Before the fix the adapter + // was not set at this point and getAdapter() threw 'Adapter not set.'. + return !$this->getAdapter()->hasTable('info'); + } +} From 67044f1bcd1fdcf491c49853882dbf4a67f06e96 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Thu, 9 Jul 2026 09:18:54 +0200 Subject: [PATCH 61/72] 5.next: Capability interfaces for reversible vs directional migrations (#1086) * Add ReversibleMigrationInterface and DirectionalMigrationInterface Introduce two marker interfaces so migrations can declare their style explicitly: - ReversibleMigrationInterface for migrations defining a single change() method (handled with the recording adapter for the down direction). - DirectionalMigrationInterface for migrations defining separate up() and down() methods. Environment now dispatches via instanceof first and keeps the existing method_exists fallback for migrations that have not adopted either interface yet, so the change is backwards compatible. The interfaces declare their method contracts via PHPDoc method tags only (not as real abstract methods), so adopting them is a single-line implements addition with no signature validation pressure. The 6.x release is expected to promote the PHPDoc method tags to real abstract method declarations and drop the method_exists fallback. * Emit capability interface in bake templates Bake-generated migrations now declare implements ReversibleMigrationInterface (change-style) or implements DirectionalMigrationInterface (up/down-style) out of the box. Updated templates: - skeleton.twig (reversible) - skeleton-anonymous.twig (reversible) - diff.twig (directional) - snapshot.twig (reversible or directional based on useChange) All bake comparison fixtures under tests/comparisons/ are updated to match the new output so the bake tests stay green. * Add AddMigrationCapabilityInterfaceRector Ship a rector rule that retrofits the capability interfaces onto existing migrations during the 5.x upgrade. For every class that extends BaseMigration (directly or transitively): - If the class defines change(), add implements ReversibleMigrationInterface. - If the class defines up() or down(), add implements DirectionalMigrationInterface. - Classes already implementing either interface are skipped. - Classes defining both styles are left alone, since the choice is a deliberate one. Abstract migration bases and anonymous migration classes are both supported so the interface propagates through inheritance and through bake's anonymous migration shape. * Document capability interfaces and the upgrade path Add a dedicated upgrade guide at docs/en/upgrades/upgrading-to-capability-interfaces.md covering motivation, per-app before/after examples for both styles, the rector-driven automatic upgrade, the manual residuals (Phinx-style bases, dynamically generated classes, third-party plugins), and the 6.x forward direction. Wire the new page into the VitePress sidebar (toc_en.json) and update the Migration Methods guide so the inline examples already include the implements clause and link to the upgrade guide. * Exclude src/Rector/ from PHPStan analysis The rector rule extends rector/rector base classes which are installed on-demand through the rector-setup composer script rather than as a permanent dev dependency. PHPStan runs before that script in CI and therefore cannot resolve AbstractRector or the Symplify value objects. The rule is type-checked by rector itself when invoked, and downstream apps that wire it into their own rector.php have rector installed locally, so excluding the directory here is the minimal fix that keeps the existing on-demand dependency pattern intact. * Update docs/en/upgrades/upgrading-to-capability-interfaces.md Co-authored-by: Mark Story * Match parent visibility of buildOptionParser() The buildOptionParser() overrides in BakeMigrationCommand and the CustomBakeMigrationDiffCommand test command were public, while the parent Command::buildOptionParser() and every other command in the package declare it protected. Rector's MakeInheritedMethodVisibilitySameAsParentRule flagged the mismatch, failing the cs-stan CI job. Align both to protected. * Fix capability-interface Rector rule edge cases Two correctness gaps in AddMigrationCapabilityInterfaceRector surfaced in review: - The rule only checked the target interface, so a class already implementing DirectionalMigrationInterface that also defined change() had ReversibleMigrationInterface added on top, producing a class implementing both mutually exclusive interfaces. Now skip any class already implementing either capability interface, keeping the rule idempotent and honoring the documented "skip if already annotated" behavior. - Directional adoption used "up() OR down()", so a one-way migration (only up() or only down()) had DirectionalMigrationInterface added. Environment calls the matching direction unconditionally for interface-based migrations, so rolling back such a migration turned a previous method_exists() no-op into a fatal undefined-method error. Only adopt the directional interface when both up() and down() exist; one-way migrations stay on the runtime fallback. Mixed-style detection (change() plus any directional method) is preserved. Docblock and the upgrade guide updated to match. --------- Co-authored-by: Mark Story --- docs/.vitepress/toc_en.json | 3 +- .../writing-migrations/migration-methods.md | 23 +- .../upgrading-to-capability-interfaces.md | 213 ++++++++++++++++++ phpstan.neon | 6 + src/Command/BakeMigrationCommand.php | 2 +- src/DirectionalMigrationInterface.php | 32 +++ src/Migration/Environment.php | 15 +- .../AddMigrationCapabilityInterfaceRector.php | 157 +++++++++++++ src/ReversibleMigrationInterface.php | 31 +++ templates/bake/config/diff.twig | 3 +- templates/bake/config/skeleton-anonymous.twig | 3 +- templates/bake/config/skeleton.twig | 3 +- templates/bake/config/snapshot.twig | 7 +- tests/TestCase/Migration/EnvironmentTest.php | 155 +++++++++++++ tests/comparisons/Create/TestCreateChange.php | 3 +- .../addRemove/the_diff_add_remove_mysql.php | 3 +- .../addRemove/the_diff_add_remove_pgsql.php | 3 +- .../initial_decimal_change_mysql.php | 3 +- .../mysql/the_diff_decimal_change_mysql.php | 3 +- .../Diff/default/the_diff_default_mysql.php | 3 +- .../Diff/default/the_diff_default_pgsql.php | 3 +- .../Diff/simple/the_diff_simple_mysql.php | 3 +- .../Diff/simple/the_diff_simple_pgsql.php | 3 +- ...d_compatible_signed_primary_keys_mysql.php | 3 +- ...incompatible_signed_primary_keys_mysql.php | 3 +- ...compatible_unsigned_primary_keys_mysql.php | 3 +- .../test_snapshot_auto_id_disabled_pgsql.php | 3 +- .../pgsql/test_snapshot_not_empty_pgsql.php | 3 +- .../pgsql/test_snapshot_plugin_blog_pgsql.php | 3 +- .../pgsql/test_snapshot_with_change_pgsql.php | 3 +- .../test_snapshot_auto_id_disabled_sqlite.php | 3 +- .../sqlite/test_snapshot_not_empty_sqlite.php | 3 +- .../test_snapshot_plugin_blog_sqlite.php | 3 +- .../test_snapshot_with_change_sqlite.php | 3 +- ...st_snapshot_auto_id_disabled_sqlserver.php | 3 +- .../test_snapshot_not_empty_sqlserver.php | 3 +- .../test_snapshot_plugin_blog_sqlserver.php | 3 +- .../test_snapshot_with_change_sqlserver.php | 3 +- .../Migration/testAddFieldWithReference.php | 3 +- tests/comparisons/Migration/testCreate.php | 3 +- .../Migration/testCreateDatetime.php | 3 +- .../Migration/testCreateDropMigration.php | 3 +- .../Migration/testCreateFieldLength.php | 3 +- .../Migration/testCreatePrimaryKey.php | 3 +- .../Migration/testCreatePrimaryKeyUuid.php | 3 +- .../Migration/testCreateWithReferences.php | 3 +- .../testCreateWithReferencesCustomTable.php | 3 +- .../comparisons/Migration/testNoContents.php | 3 +- .../test_snapshot_auto_id_disabled.php | 3 +- .../Migration/test_snapshot_not_empty.php | 3 +- .../Migration/test_snapshot_plugin_blog.php | 3 +- ...t_snapshot_postgres_timestamp_tz_pgsql.php | 3 +- ...auto_id_compatible_signed_primary_keys.php | 3 +- ...to_id_incompatible_signed_primary_keys.php | 3 +- ..._id_incompatible_unsigned_primary_keys.php | 3 +- .../Migration/test_snapshot_with_change.php | 3 +- ...st_snapshot_with_non_default_collation.php | 3 +- .../CustomBakeMigrationDiffCommand.php | 2 +- 58 files changed, 729 insertions(+), 55 deletions(-) create mode 100644 docs/en/upgrades/upgrading-to-capability-interfaces.md create mode 100644 src/DirectionalMigrationInterface.php create mode 100644 src/Rector/AddMigrationCapabilityInterfaceRector.php create mode 100644 src/ReversibleMigrationInterface.php diff --git a/docs/.vitepress/toc_en.json b/docs/.vitepress/toc_en.json index a50f01a4e..91d6d65e8 100644 --- a/docs/.vitepress/toc_en.json +++ b/docs/.vitepress/toc_en.json @@ -37,7 +37,8 @@ "collapsed": true, "items": [ { "text": "Upgrading from 4.x to 5.x", "link": "/upgrades/upgrading-from-4-x" }, - { "text": "Upgrading to the Builtin Backend", "link": "/upgrades/upgrading-to-builtin-backend" } + { "text": "Upgrading to the Builtin Backend", "link": "/upgrades/upgrading-to-builtin-backend" }, + { "text": "Upgrading to Capability Interfaces", "link": "/upgrades/upgrading-to-capability-interfaces" } ] } ] diff --git a/docs/en/guides/writing-migrations/migration-methods.md b/docs/en/guides/writing-migrations/migration-methods.md index 2ac07c995..c9b3c44bc 100644 --- a/docs/en/guides/writing-migrations/migration-methods.md +++ b/docs/en/guides/writing-migrations/migration-methods.md @@ -1,5 +1,19 @@ # Migration Methods +A migration declares its style by implementing one of two capability interfaces: + +- `Migrations\ReversibleMigrationInterface` for migrations that define a + single `change()` method. +- `Migrations\DirectionalMigrationInterface` for migrations that define + separate `up()` and `down()` methods. + +A migration implements **one** of the two, never both. Bake-generated +migrations already include the right `implements` clause. Migrations from +older versions of cakephp/migrations keep working without the interface +through a `method_exists()` fallback; see +[Upgrading to Capability Interfaces](/upgrades/upgrading-to-capability-interfaces) +for the adoption path. + ## The Change Method Migrations supports 'reversible migrations'. In many scenarios, you only need @@ -10,8 +24,9 @@ rollback operations for you. For example: table('products') + ->addColumn('name', 'string') + ->create(); + } +} +``` + +After: + +```php +table('products') + ->addColumn('name', 'string') + ->create(); + } +} +``` + +### Directional migration (defines `up()` and `down()`) + +Before: + +```php +execute('UPDATE orders SET total = ...'); + } + + public function down(): void + { + $this->execute('UPDATE orders SET total = NULL'); + } +} +``` + +After: + +```php +execute('UPDATE orders SET total = ...'); + } + + public function down(): void + { + $this->execute('UPDATE orders SET total = NULL'); + } +} +``` + +### Anonymous migrations + +Anonymous migrations get the same treatment: + +```php +return new class extends BaseMigration implements ReversibleMigrationInterface +{ + public function change(): void + { + } +}; +``` + +## Automated upgrade with rector + +cakephp/migrations ships a rector rule that adds the right `implements` clause to every migration in your `config/Migrations/` folder. + +Add the following to your `rector.php`: + +```php +use Migrations\Rector\AddMigrationCapabilityInterfaceRector; +use Rector\Config\RectorConfig; + +return RectorConfig::configure() + ->withPaths([ + __DIR__ . '/config/Migrations', + ]) + ->withRules([ + AddMigrationCapabilityInterfaceRector::class, + ]); +``` + +Then run rector: + +```bash +vendor/bin/rector process --config=rector.php +``` + +What the rule does: + +- For every class extending `Migrations\BaseMigration` (directly or transitively): + - If the class defines `change()`, add `implements ReversibleMigrationInterface`. + - If the class defines both `up()` and `down()`, add `implements DirectionalMigrationInterface`. +- One-way migrations that define only `up()` or only `down()` are left untouched. They keep working through the `method_exists()` fallback; adding `DirectionalMigrationInterface` would make `Environment` call the missing direction unconditionally and turn a rollback no-op into a fatal error. +- Classes that already implement either capability interface are skipped. +- Classes that define both `change()` and `up()`/`down()` are skipped — these are user errors that need a deliberate decision. + +### Optional: combine with built-in rector rules + +If you also want to normalize visibility and return types on your migration methods (the shape 6.x will expect), compose with rector's built-in sets: + +```php +use Migrations\Rector\AddMigrationCapabilityInterfaceRector; +use Rector\Config\RectorConfig; +use Rector\Set\ValueObject\SetList; + +return RectorConfig::configure() + ->withPaths([ + __DIR__ . '/config/Migrations', + ]) + ->withRules([ + AddMigrationCapabilityInterfaceRector::class, + ]) + ->withSets([ + SetList::TYPE_DECLARATION, + ]); +``` + +::: warning Only point rector at your migrations folder +Migration paths use scoped rector configs by default; pointing rector at `src/` or `tests/` will apply unrelated transformations. Keep the path list narrow. +::: + +## Manual work that remains after rector + +- **Migrations not on `BaseMigration`.** Anything still on a legacy Phinx `AbstractMigration` fork or a custom base that does not extend `BaseMigration` is skipped. Add the `implements` clause by hand. +- **Dynamically generated migration classes** (eval'd test fixtures, factories). Rector cannot see them. Add the `implements` clause at the generation site. +- **Custom base classes that themselves declare `change()` / `up()` / `down()`.** Rector adds the interface to the base class once. If the base lives in a third-party plugin you do not control, either implement the capability interface on the leaf class or PR the plugin upstream. +- **Bake-generated migrations from older versions.** Bake templates emit the `implements` clause out of the box from 5.next; older bakes do not. Rector cleans those up in one pass. + +## Forward direction + +The 6.x release is expected to: + +- Promote the PHPDoc `@method` declarations on the capability interfaces to real abstract method declarations. +- Remove the `method_exists()` fallback in `Environment`. + +Running rector now means the 6.x bump is a no-op for your migration files. diff --git a/phpstan.neon b/phpstan.neon index b35bf0118..c95484c60 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,6 +5,12 @@ parameters: level: 8 paths: - src/ + excludePaths: + # rector/rector is installed on-demand via the rector-setup composer + # script, so PHPStan cannot resolve AbstractRector or the Symplify + # value objects during the main analysis run. The rule is autoloaded + # for downstream apps and type-checked by rector itself when invoked. + - src/Rector/ bootstrapFiles: - tests/bootstrap.php ignoreErrors: diff --git a/src/Command/BakeMigrationCommand.php b/src/Command/BakeMigrationCommand.php index 8b59fc715..d49a4c6c8 100644 --- a/src/Command/BakeMigrationCommand.php +++ b/src/Command/BakeMigrationCommand.php @@ -245,7 +245,7 @@ public function getOptionParser(): ConsoleOptionParser /** * @inheritDoc */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { $parser = parent::buildOptionParser($parser); diff --git a/src/DirectionalMigrationInterface.php b/src/DirectionalMigrationInterface.php new file mode 100644 index 000000000..a5004d105 --- /dev/null +++ b/src/DirectionalMigrationInterface.php @@ -0,0 +1,32 @@ +{MigrationInterface::CHANGE}(); } + } elseif ($migration instanceof DirectionalMigrationInterface) { + $direction === MigrationInterface::UP ? $migration->up() : $migration->down(); } elseif (method_exists($migration, $direction)) { $migration->{$direction}(); } diff --git a/src/Rector/AddMigrationCapabilityInterfaceRector.php b/src/Rector/AddMigrationCapabilityInterfaceRector.php new file mode 100644 index 000000000..9ef0ab7a6 --- /dev/null +++ b/src/Rector/AddMigrationCapabilityInterfaceRector.php @@ -0,0 +1,157 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param \PhpParser\Node\Stmt\Class_ $node + */ + public function refactor(Node $node): ?Node + { + $classReflection = ScopeFetcher::fetch($node)->getClassReflection(); + if (!$classReflection instanceof ClassReflection) { + return null; + } + + // Both abstract bases and anonymous migrations are valid targets: abstract + // bases propagate the interface to every leaf, and anonymous migrations are + // baked the same way concrete ones are. The only excluded type is + // BaseMigration itself, since it must remain style-agnostic. + if ( + $classReflection->getName() === BaseMigration::class + || !$classReflection->isSubclassOf(BaseMigration::class) + ) { + return null; + } + + // Already annotated with either capability interface: leave it alone so the + // rule stays idempotent and never ends up adding the second, conflicting one. + if ( + $classReflection->implementsInterface(ReversibleMigrationInterface::class) + || $classReflection->implementsInterface(DirectionalMigrationInterface::class) + ) { + return null; + } + + $hasChange = $this->classHasMethod($node, MigrationInterface::CHANGE); + $hasUp = $this->classHasMethod($node, MigrationInterface::UP); + $hasDown = $this->classHasMethod($node, MigrationInterface::DOWN); + + // Mixed style is user error. Leave it untouched so the developer picks deliberately. + if ($hasChange && ($hasUp || $hasDown)) { + return null; + } + + // Only adopt the directional interface when BOTH methods are present. A one-way + // migration (only up() or only down()) must stay on the Environment method_exists() + // fallback: DirectionalMigrationInterface makes Environment call the missing + // direction unconditionally, turning a rollback no-op into a fatal error. + $targetInterface = null; + if ($hasChange) { + $targetInterface = ReversibleMigrationInterface::class; + } elseif ($hasUp && $hasDown) { + $targetInterface = DirectionalMigrationInterface::class; + } + + if ($targetInterface === null) { + return null; + } + + $node->implements[] = new FullyQualified($targetInterface); + + return $node; + } + + /** + * @param \PhpParser\Node\Stmt\Class_ $class Class node to inspect. + * @param string $methodName Method name to look for on the class itself. + * @return bool + */ + private function classHasMethod(Class_ $class, string $methodName): bool + { + foreach ($class->getMethods() as $method) { + if ($this->getName($method) === $methodName) { + return true; + } + } + + return false; + } +} diff --git a/src/ReversibleMigrationInterface.php b/src/ReversibleMigrationInterface.php new file mode 100644 index 000000000..ba0d01a4c --- /dev/null +++ b/src/ReversibleMigrationInterface.php @@ -0,0 +1,31 @@ +assertTrue($migration->executed); } + public function testExecutingAReversibleInterfaceMigrationUp(): void + { + $adapterStub = $this->getMockBuilder(AbstractAdapter::class) + ->setConstructorArgs([[]]) + ->getMock(); + $adapterStub->expects($this->once()) + ->method('migrated') + ->willReturn($adapterStub); + + $this->environment->setAdapter($adapterStub); + + $migration = new class (20260513120000) extends BaseMigration implements ReversibleMigrationInterface { + public bool $executed = false; + + public function change(): void + { + $this->executed = true; + } + }; + + $this->environment->executeMigration($migration, MigrationInterface::UP); + $this->assertTrue($migration->executed); + } + + public function testExecutingAReversibleInterfaceMigrationDown(): void + { + $adapterStub = $this->getMockBuilder(AbstractAdapter::class) + ->setConstructorArgs([[]]) + ->getMock(); + $adapterStub->expects($this->once()) + ->method('migrated') + ->willReturn($adapterStub); + + $this->environment->setAdapter($adapterStub); + + $migration = new class (20260513120000) extends BaseMigration implements ReversibleMigrationInterface { + public bool $executed = false; + + public function change(): void + { + $this->executed = true; + } + }; + + $this->environment->executeMigration($migration, MigrationInterface::DOWN); + $this->assertTrue($migration->executed); + } + + public function testExecutingADirectionalInterfaceMigrationUp(): void + { + $adapterStub = $this->getMockBuilder(AbstractAdapter::class) + ->setConstructorArgs([[]]) + ->getMock(); + $adapterStub->expects($this->once()) + ->method('migrated') + ->willReturn($adapterStub); + + $this->environment->setAdapter($adapterStub); + + $migration = new class (20260513120000) extends BaseMigration implements DirectionalMigrationInterface { + public bool $upExecuted = false; + + public bool $downExecuted = false; + + public function up(): void + { + $this->upExecuted = true; + } + + public function down(): void + { + $this->downExecuted = true; + } + }; + + $this->environment->executeMigration($migration, MigrationInterface::UP); + $this->assertTrue($migration->upExecuted); + $this->assertFalse($migration->downExecuted); + } + + public function testExecutingADirectionalInterfaceMigrationDown(): void + { + $adapterStub = $this->getMockBuilder(AbstractAdapter::class) + ->setConstructorArgs([[]]) + ->getMock(); + $adapterStub->expects($this->once()) + ->method('migrated') + ->willReturn($adapterStub); + + $this->environment->setAdapter($adapterStub); + + $migration = new class (20260513120000) extends BaseMigration implements DirectionalMigrationInterface { + public bool $upExecuted = false; + + public bool $downExecuted = false; + + public function up(): void + { + $this->upExecuted = true; + } + + public function down(): void + { + $this->downExecuted = true; + } + }; + + $this->environment->executeMigration($migration, MigrationInterface::DOWN); + $this->assertTrue($migration->downExecuted); + $this->assertFalse($migration->upExecuted); + } + + /** + * If a class declares DirectionalMigrationInterface, dispatch must use + * up()/down() even if the class happens to define a change() method — + * the interface declaration wins over the legacy method_exists check. + */ + public function testDirectionalInterfaceWinsOverChangeMethod(): void + { + $adapterStub = $this->getMockBuilder(AbstractAdapter::class) + ->setConstructorArgs([[]]) + ->getMock(); + $adapterStub->expects($this->once()) + ->method('migrated') + ->willReturn($adapterStub); + + $this->environment->setAdapter($adapterStub); + + $migration = new class (20260513120000) extends BaseMigration implements DirectionalMigrationInterface { + public bool $upExecuted = false; + + public bool $changeExecuted = false; + + public function up(): void + { + $this->upExecuted = true; + } + + public function down(): void + { + } + + public function change(): void + { + $this->changeExecuted = true; + } + }; + + $this->environment->executeMigration($migration, MigrationInterface::UP); + $this->assertTrue($migration->upExecuted); + $this->assertFalse($migration->changeExecuted); + } + public function testExecutingAFakeMigration(): void { // stub adapter diff --git a/tests/comparisons/Create/TestCreateChange.php b/tests/comparisons/Create/TestCreateChange.php index 071aa889f..3bae255fa 100644 --- a/tests/comparisons/Create/TestCreateChange.php +++ b/tests/comparisons/Create/TestCreateChange.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class TestCreateChange extends BaseMigration +class TestCreateChange extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php index a6dd8bf6a..647c7b77f 100644 --- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php +++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffAddRemoveMysql extends BaseMigration +class TheDiffAddRemoveMysql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php b/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php index 28fda29b9..2430bb7ad 100644 --- a/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php +++ b/tests/comparisons/Diff/addRemove/the_diff_add_remove_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffAddRemovePgsql extends BaseMigration +class TheDiffAddRemovePgsql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php index 984829c9c..a8a33ffe2 100644 --- a/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php +++ b/tests/comparisons/Diff/decimalChange/initial_decimal_change_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class InitialDecimalChangeMysql extends BaseMigration +class InitialDecimalChangeMysql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php index a32449e17..dfeb91d2e 100644 --- a/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php +++ b/tests/comparisons/Diff/decimalChange/mysql/the_diff_decimal_change_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffDecimalChangeMysql extends BaseMigration +class TheDiffDecimalChangeMysql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/default/the_diff_default_mysql.php b/tests/comparisons/Diff/default/the_diff_default_mysql.php index 679efe87b..25e1852ea 100644 --- a/tests/comparisons/Diff/default/the_diff_default_mysql.php +++ b/tests/comparisons/Diff/default/the_diff_default_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffDefaultMysql extends BaseMigration +class TheDiffDefaultMysql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/default/the_diff_default_pgsql.php b/tests/comparisons/Diff/default/the_diff_default_pgsql.php index 52e1f0766..521e9f4e7 100644 --- a/tests/comparisons/Diff/default/the_diff_default_pgsql.php +++ b/tests/comparisons/Diff/default/the_diff_default_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffDefaultPgsql extends BaseMigration +class TheDiffDefaultPgsql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php index a8f69ca98..47e827109 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_mysql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffSimpleMysql extends BaseMigration +class TheDiffSimpleMysql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php b/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php index 948bcc954..04807a8b0 100644 --- a/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php +++ b/tests/comparisons/Diff/simple/the_diff_simple_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffSimplePgsql extends BaseMigration +class TheDiffSimplePgsql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php index 4e5e45848..28b89d97f 100644 --- a/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdCompatibleSignedPrimaryKeys/the_diff_with_auto_id_compatible_signed_primary_keys_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffWithAutoIdCompatibleSignedPrimaryKeysMysql extends BaseMigration +class TheDiffWithAutoIdCompatibleSignedPrimaryKeysMysql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php index 1a645545d..76c3811a6 100644 --- a/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdIncompatibleSignedPrimaryKeys/the_diff_with_auto_id_incompatible_signed_primary_keys_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffWithAutoIdIncompatibleSignedPrimaryKeysMysql extends BaseMigration +class TheDiffWithAutoIdIncompatibleSignedPrimaryKeysMysql extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; diff --git a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php index 5d06abc3e..07fe3c208 100644 --- a/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php +++ b/tests/comparisons/Diff/withAutoIdIncompatibleUnsignedPrimaryKeys/the_diff_with_auto_id_incompatible_unsigned_primary_keys_mysql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TheDiffWithAutoIdIncompatibleUnsignedPrimaryKeysMysql extends BaseMigration +class TheDiffWithAutoIdIncompatibleUnsignedPrimaryKeysMysql extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; 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 bbe494204..ad8c3f905 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 @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotAutoIdDisabledPgsql extends BaseMigration +class TestSnapshotAutoIdDisabledPgsql extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; 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 8ff44ed60..c1c03a32d 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_not_empty_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotNotEmptyPgsql extends BaseMigration +class TestSnapshotNotEmptyPgsql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 25f27019b..9407d2fd6 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_plugin_blog_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotPluginBlogPgsql extends BaseMigration +class TestSnapshotPluginBlogPgsql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 2c3cb4f59..7000d9705 100644 --- a/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php +++ b/tests/comparisons/Migration/pgsql/test_snapshot_with_change_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class TestSnapshotWithChangePgsql extends BaseMigration +class TestSnapshotWithChangePgsql extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. 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 12261cdd3..43f71f8c5 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 @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotAutoIdDisabledSqlite extends BaseMigration +class TestSnapshotAutoIdDisabledSqlite extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; 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 bef6d8cea..4b6458330 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_not_empty_sqlite.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotNotEmptySqlite extends BaseMigration +class TestSnapshotNotEmptySqlite extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 f9f840281..f724bb157 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_plugin_blog_sqlite.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotPluginBlogSqlite extends BaseMigration +class TestSnapshotPluginBlogSqlite extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 857bbde1d..fc0eb8b5f 100644 --- a/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php +++ b/tests/comparisons/Migration/sqlite/test_snapshot_with_change_sqlite.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class TestSnapshotWithChangeSqlite extends BaseMigration +class TestSnapshotWithChangeSqlite extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. 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 47b630bfd..1e28edab6 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 @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotAutoIdDisabledSqlserver extends BaseMigration +class TestSnapshotAutoIdDisabledSqlserver extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; 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 aa2bc9a34..aaa9653b7 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_not_empty_sqlserver.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotNotEmptySqlserver extends BaseMigration +class TestSnapshotNotEmptySqlserver extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 8e49310d4..5ae1d3f66 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_plugin_blog_sqlserver.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotPluginBlogSqlserver extends BaseMigration +class TestSnapshotPluginBlogSqlserver extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 f1282faf4..c4c5945ef 100644 --- a/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php +++ b/tests/comparisons/Migration/sqlserver/test_snapshot_with_change_sqlserver.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class TestSnapshotWithChangeSqlserver extends BaseMigration +class TestSnapshotWithChangeSqlserver extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testAddFieldWithReference.php b/tests/comparisons/Migration/testAddFieldWithReference.php index b6c5ccfb2..6b12e9ec1 100644 --- a/tests/comparisons/Migration/testAddFieldWithReference.php +++ b/tests/comparisons/Migration/testAddFieldWithReference.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class AddCategoryIdToProducts extends BaseMigration +class AddCategoryIdToProducts extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testCreate.php b/tests/comparisons/Migration/testCreate.php index 39bc88392..cc96d73a2 100644 --- a/tests/comparisons/Migration/testCreate.php +++ b/tests/comparisons/Migration/testCreate.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreateUsers extends BaseMigration +class CreateUsers extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testCreateDatetime.php b/tests/comparisons/Migration/testCreateDatetime.php index 0547f6851..2580e9a6a 100644 --- a/tests/comparisons/Migration/testCreateDatetime.php +++ b/tests/comparisons/Migration/testCreateDatetime.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreateUsers extends BaseMigration +class CreateUsers extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testCreateDropMigration.php b/tests/comparisons/Migration/testCreateDropMigration.php index a7550b664..bf0a8b558 100644 --- a/tests/comparisons/Migration/testCreateDropMigration.php +++ b/tests/comparisons/Migration/testCreateDropMigration.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class DropUsers extends BaseMigration +class DropUsers extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testCreateFieldLength.php b/tests/comparisons/Migration/testCreateFieldLength.php index 43220d035..73fce4678 100644 --- a/tests/comparisons/Migration/testCreateFieldLength.php +++ b/tests/comparisons/Migration/testCreateFieldLength.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreateUsers extends BaseMigration +class CreateUsers extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testCreatePrimaryKey.php b/tests/comparisons/Migration/testCreatePrimaryKey.php index 44efb2631..e62cf434d 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKey.php +++ b/tests/comparisons/Migration/testCreatePrimaryKey.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreateUsers extends BaseMigration +class CreateUsers extends BaseMigration implements ReversibleMigrationInterface { public bool $autoId = false; diff --git a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php index de8c21792..6fca2f3e9 100644 --- a/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php +++ b/tests/comparisons/Migration/testCreatePrimaryKeyUuid.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreateUsers extends BaseMigration +class CreateUsers extends BaseMigration implements ReversibleMigrationInterface { public bool $autoId = false; diff --git a/tests/comparisons/Migration/testCreateWithReferences.php b/tests/comparisons/Migration/testCreateWithReferences.php index 5537e448e..03115c1fa 100644 --- a/tests/comparisons/Migration/testCreateWithReferences.php +++ b/tests/comparisons/Migration/testCreateWithReferences.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreatePosts extends BaseMigration +class CreatePosts extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php index 6897dd2aa..9f24d2bd7 100644 --- a/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php +++ b/tests/comparisons/Migration/testCreateWithReferencesCustomTable.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class CreateArticles extends BaseMigration +class CreateArticles extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/testNoContents.php b/tests/comparisons/Migration/testNoContents.php index 19c2524b5..8803c6971 100644 --- a/tests/comparisons/Migration/testNoContents.php +++ b/tests/comparisons/Migration/testNoContents.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class NoContents extends BaseMigration +class NoContents extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. diff --git a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php index c8d07b215..4a31bff0d 100644 --- a/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php +++ b/tests/comparisons/Migration/test_snapshot_auto_id_disabled.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotAutoIdDisabled extends BaseMigration +class TestSnapshotAutoIdDisabled extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; diff --git a/tests/comparisons/Migration/test_snapshot_not_empty.php b/tests/comparisons/Migration/test_snapshot_not_empty.php index 4bde415d7..2e9a04e4d 100644 --- a/tests/comparisons/Migration/test_snapshot_not_empty.php +++ b/tests/comparisons/Migration/test_snapshot_not_empty.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotNotEmpty extends BaseMigration +class TestSnapshotNotEmpty extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/comparisons/Migration/test_snapshot_plugin_blog.php b/tests/comparisons/Migration/test_snapshot_plugin_blog.php index d33673594..fa0d8c947 100644 --- a/tests/comparisons/Migration/test_snapshot_plugin_blog.php +++ b/tests/comparisons/Migration/test_snapshot_plugin_blog.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotPluginBlog extends BaseMigration +class TestSnapshotPluginBlog extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 e4f7da4f7..94fd8df87 100644 --- a/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php +++ b/tests/comparisons/Migration/test_snapshot_postgres_timestamp_tz_pgsql.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotPostgresTimestampTzPgsql extends BaseMigration +class TestSnapshotPostgresTimestampTzPgsql extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. 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 3da752b24..7551e0d27 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 @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotWithAutoIdCompatibleSignedPrimaryKeys extends BaseMigration +class TestSnapshotWithAutoIdCompatibleSignedPrimaryKeys extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; 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 13f8327da..bde4dfc13 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 @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotWithAutoIdIncompatibleSignedPrimaryKeys extends BaseMigration +class TestSnapshotWithAutoIdIncompatibleSignedPrimaryKeys extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; 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 bdf099943..7e2f184a3 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 @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotWithAutoIdIncompatibleUnsignedPrimaryKeys extends BaseMigration +class TestSnapshotWithAutoIdIncompatibleUnsignedPrimaryKeys extends BaseMigration implements DirectionalMigrationInterface { public bool $autoId = false; diff --git a/tests/comparisons/Migration/test_snapshot_with_change.php b/tests/comparisons/Migration/test_snapshot_with_change.php index d4a432337..eee87f778 100644 --- a/tests/comparisons/Migration/test_snapshot_with_change.php +++ b/tests/comparisons/Migration/test_snapshot_with_change.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\ReversibleMigrationInterface; -class TestSnapshotWithChange extends BaseMigration +class TestSnapshotWithChange extends BaseMigration implements ReversibleMigrationInterface { /** * Change Method. 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 c48d00a4e..544254be4 100644 --- a/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php +++ b/tests/comparisons/Migration/test_snapshot_with_non_default_collation.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotWithNonDefaultCollation extends BaseMigration +class TestSnapshotWithNonDefaultCollation extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method. diff --git a/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php b/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php index e7bc5ae24..c52597f6c 100644 --- a/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php +++ b/tests/test_app/App/Command/CustomBakeMigrationDiffCommand.php @@ -23,7 +23,7 @@ public static function defaultName(): string /** * @inheritDoc */ - public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { return parent::buildOptionParser($parser) ->addOption('test-target-folder') From 6f5444bb64bf89454a1eeabc25ff030fab796345 Mon Sep 17 00:00:00 2001 From: Jamison Bryant Date: Fri, 17 Jul 2026 14:44:35 -0400 Subject: [PATCH 62/72] Keep ON UPDATE clause when baking migrations (#1103) CakePHP reflects `ON UPDATE CURRENT_TIMESTAMP` on datetime/timestamp columns as an `onUpdate` column attribute, but MigrationHelper dropped it in two places: the valid-options list in attributes() and the wanted- options list in getColumnOption(). The clause never reached the template, so any column declaring it lost it when snapshotted. Databases rebuilt from a snapshot then silently stopped auto-updating those columns on writes that bypass the ORM, while existing databases kept the real schema. Let the attribute through both filters and translate it to Phinx's `update` option, mirroring the existing collate/collation handling. Both filters drop the attribute when empty, since Column::toArray() always emits an onUpdate key and Column::setUpdate() is not nullable. * Cover the attributes() half of the ON UPDATE fix The two existing tests fed getColumnOption() a hand-built array, which bypasses attributes() entirely. Removing 'onUpdate' from the valid-options list in attributes() therefore broke snapshot output with zero test failures, even though it is one of the two changes the fix depends on. Add a test that drives attributes() directly and one that walks the whole snapshot path (columns() -> getColumnOption()). Both build the schema by hand rather than reflecting it, so they run on every driver. * Add snapshot regression test for ON UPDATE The MigrationHelper tests build their schema by hand, so they prove the helper emits the option but not that a baked snapshot keeps it. Cover the end-to-end path: reflect a real ON UPDATE column, bake a snapshot, and compare the generated migration against a checked-in file. Gated on MySQL because only MysqlSchemaDialect reflects onUpdate; the other drivers never produce the attribute. Mirrors the existing non-default-collation test, which faces the same constraint. The comparison file differs from the plain snapshot by a single 'update' => 'CURRENT_TIMESTAMP' line on users.updated, while users.created stays bare, so the test also pins down that the option is not applied to columns that never declared it. --- src/View/Helper/MigrationHelper.php | 15 + .../BakeMigrationSnapshotCommandTest.php | 21 + .../View/Helper/MigrationHelperTest.php | 94 +++++ .../test_snapshot_with_on_update.php | 387 ++++++++++++++++++ 4 files changed, 517 insertions(+) create mode 100644 tests/comparisons/Migration/test_snapshot_with_on_update.php diff --git a/src/View/Helper/MigrationHelper.php b/src/View/Helper/MigrationHelper.php index 3a6c52560..5cc4cd399 100644 --- a/src/View/Helper/MigrationHelper.php +++ b/src/View/Helper/MigrationHelper.php @@ -373,6 +373,7 @@ public function getColumnOption(array $options): array 'after', 'collate', 'fixed', + 'onUpdate', ]); $columnOptions = array_intersect_key($options, $wantedOptions); if (empty($columnOptions['comment'])) { @@ -384,6 +385,9 @@ public function getColumnOption(array $options): array if (empty($columnOptions['collate'])) { unset($columnOptions['collate']); } + if (empty($columnOptions['onUpdate'])) { + unset($columnOptions['onUpdate']); + } // isset() returns false for null values, so this handles both missing and null cases if (!isset($columnOptions['fixed'])) { unset($columnOptions['fixed']); @@ -406,6 +410,12 @@ public function getColumnOption(array $options): array unset($columnOptions['collate']); } + if (!empty($columnOptions['onUpdate'])) { + // Phinx uses 'update' not 'onUpdate' + $columnOptions['update'] = $columnOptions['onUpdate']; + unset($columnOptions['onUpdate']); + } + // Handle precision/scale conversion between CakePHP's TableSchema format and SQL standard format. // TableSchema uses: length=total digits, precision=decimal places // Migrations uses SQL standard: precision=total digits, scale=decimal places @@ -491,6 +501,7 @@ public function attributes(TableSchemaInterface|string $table, string $column): 'signed', 'properties', 'autoIncrement', 'unique', 'collate', 'fixed', + 'onUpdate', ]; $attributes = []; @@ -540,6 +551,10 @@ public function attributes(TableSchemaInterface|string $table, string $column): unset($attributes['collate']); } + if (empty($attributes['onUpdate'])) { + unset($attributes['onUpdate']); + } + ksort($attributes); return $attributes; diff --git a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php index ac28573bb..a703f7a5d 100644 --- a/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php +++ b/tests/TestCase/Command/BakeMigrationSnapshotCommandTest.php @@ -392,4 +392,25 @@ public function testSnapshotWithNonDefaultCollation(): void $this->runSnapshotTest('WithNonDefaultCollation', '-p SimpleSnapshot'); $connection->execute('ALTER TABLE events MODIFY title VARCHAR(255)'); } + + /** + * Tests that an ON UPDATE clause is kept in the initial snapshot. + */ + public function testSnapshotWithOnUpdate(): void + { + $this->skipIf(env('DB') !== 'mysql'); + $this->loadPlugins(['SimpleSnapshot']); + + $this->migrationPath = ROOT . DS . 'Plugin' . DS . 'SimpleSnapshot' . DS . 'config' . DS . 'Migrations' . DS; + + $connection = ConnectionManager::get('test'); + assert($connection instanceof Connection); + + $connection->execute('ALTER TABLE users MODIFY updated TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP'); + try { + $this->runSnapshotTest('WithOnUpdate', '-p SimpleSnapshot'); + } finally { + $connection->execute('ALTER TABLE users MODIFY updated TIMESTAMP NULL DEFAULT NULL'); + } + } } diff --git a/tests/TestCase/View/Helper/MigrationHelperTest.php b/tests/TestCase/View/Helper/MigrationHelperTest.php index ad1a5073b..76b1974c5 100644 --- a/tests/TestCase/View/Helper/MigrationHelperTest.php +++ b/tests/TestCase/View/Helper/MigrationHelperTest.php @@ -381,6 +381,57 @@ public function testAttributes(): void $this->assertEquals($attributes, $result); } + /** + * Test that attributes() preserves the onUpdate attribute + * + * `onUpdate` is a first-class column key in CakePHP's TableSchema for datetime + * and timestamp types, so attributes() must not filter it out. The schema is + * built by hand rather than reflected so this holds for every driver. + */ + public function testAttributesPreservesOnUpdate(): void + { + $tableSchema = new TableSchema('on_update_columns'); + $tableSchema->addColumn('modified', [ + 'type' => 'datetime', + 'null' => false, + 'onUpdate' => 'CURRENT_TIMESTAMP', + ]); + $tableSchema->addColumn('plain', [ + 'type' => 'datetime', + 'null' => true, + ]); + + $modified = $this->helper->attributes($tableSchema, 'modified'); + $this->assertArrayHasKey('onUpdate', $modified, 'onUpdate should survive attributes()'); + $this->assertSame('CURRENT_TIMESTAMP', $modified['onUpdate']); + + $plain = $this->helper->attributes($tableSchema, 'plain'); + $this->assertArrayNotHasKey('onUpdate', $plain, 'columns without ON UPDATE should not gain the key'); + } + + /** + * Test that a column with an ON UPDATE clause bakes the Phinx `update` option + * + * Guards the whole snapshot path: attributes() must preserve `onUpdate` and + * getColumnOption() must translate it to Phinx's `update` option. Either half + * regressing silently drops the clause from generated migrations. + */ + public function testColumnsRendersOnUpdateAsUpdateOption(): void + { + $tableSchema = new TableSchema('on_update_columns'); + $tableSchema->addColumn('modified', [ + 'type' => 'datetime', + 'null' => false, + 'onUpdate' => 'CURRENT_TIMESTAMP', + ]); + + $columns = $this->helper->columns($tableSchema); + $options = $this->helper->getColumnOption($columns['modified']['options']); + + $this->assertArrayNotHasKey('onUpdate', $options); + $this->assertSame('CURRENT_TIMESTAMP', $options['update']); + } + public function testStringifyList(): void { $this->assertSame('', $this->helper->stringifyList([])); @@ -459,6 +510,49 @@ public function testGetColumnOptionConvertsCollateToCollation(): void $this->assertSame('en_US.UTF-8', $result['collation']); } + /** + * Test that getColumnOption converts onUpdate to update + * + * CakePHP reflects `ON UPDATE` clauses as 'onUpdate', but Phinx uses the + * 'update' column option, so this must be converted for the clause to + * survive a snapshot. + */ + public function testGetColumnOptionConvertsOnUpdateToUpdate(): void + { + $options = [ + 'null' => true, + 'default' => null, + 'onUpdate' => 'CURRENT_TIMESTAMP', + ]; + + $result = $this->helper->getColumnOption($options); + + $this->assertArrayNotHasKey('onUpdate', $result, 'onUpdate should be converted to update'); + $this->assertArrayHasKey('update', $result, 'update should be set from onUpdate value'); + $this->assertSame('CURRENT_TIMESTAMP', $result['update']); + } + + /** + * Test that getColumnOption removes null onUpdate + * + * Column::toArray() always emits an 'onUpdate' key, so columns without an + * `ON UPDATE` clause carry a null. Column::setUpdate() is not nullable, so + * passing it through would be a TypeError. + */ + public function testGetColumnOptionRemovesNullOnUpdate(): void + { + $options = [ + 'null' => true, + 'default' => null, + 'onUpdate' => null, + ]; + + $result = $this->helper->getColumnOption($options); + + $this->assertArrayNotHasKey('onUpdate', $result, 'onUpdate => null should be removed'); + $this->assertArrayNotHasKey('update', $result, 'update should not be set when onUpdate is null'); + } + /** * Test that getColumnOption includes the fixed option for binary columns */ diff --git a/tests/comparisons/Migration/test_snapshot_with_on_update.php b/tests/comparisons/Migration/test_snapshot_with_on_update.php new file mode 100644 index 000000000..a3e4c9ba4 --- /dev/null +++ b/tests/comparisons/Migration/test_snapshot_with_on_update.php @@ -0,0 +1,387 @@ +table('articles') + ->addColumn('title', 'string', [ + 'comment' => 'Article title', + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('category_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->addColumn('product_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->addColumn('note', 'string', [ + 'default' => '7.4', + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('counter', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->addColumn('active', 'boolean', [ + 'default' => false, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('created', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('modified', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addIndex( + $this->index('category_id') + ->setName('articles_category_fk') + ) + ->addIndex( + $this->index('title') + ->setName('articles_title_idx') + ) + ->create(); + + $this->table('categories') + ->addColumn('parent_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->addColumn('title', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('slug', 'string', [ + 'default' => null, + 'limit' => 100, + 'null' => true, + ]) + ->addColumn('created', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('modified', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addIndex( + $this->index('slug') + ->setName('categories_slug_unique') + ->setType('unique') + ) + ->create(); + + $this->table('composite_pks', ['id' => false, 'primary_key' => ['id', 'name']]) + ->addColumn('id', 'uuid', [ + 'default' => 'a4950df3-515f-474c-be4c-6a027c1957e7', + 'limit' => null, + 'null' => false, + ]) + ->addColumn('name', 'string', [ + 'default' => '', + 'limit' => 10, + 'null' => false, + ]) + ->create(); + + $this->table('events') + ->addColumn('title', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('description', 'text', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('published', 'string', [ + 'default' => 'N', + 'limit' => 1, + 'null' => true, + ]) + ->create(); + + $this->table('orders') + ->addColumn('product_category', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => false, + 'signed' => false, + ]) + ->addColumn('product_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => false, + 'signed' => false, + ]) + ->addIndex( + $this->index([ + 'product_category', + 'product_id', + ]) + ->setName('orders_product_category_idx') + ) + ->create(); + + $this->table('parts') + ->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('number', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->create(); + + $this->table('products') + ->addColumn('title', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('slug', 'string', [ + 'default' => null, + 'limit' => 100, + 'null' => true, + ]) + ->addColumn('category_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->addColumn('created', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('modified', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addIndex( + $this->index('slug') + ->setName('products_slug_unique') + ->setType('unique') + ) + ->addIndex( + $this->index([ + 'category_id', + 'id', + ]) + ->setName('products_category_unique') + ->setType('unique') + ) + ->addIndex( + $this->index('title') + ->setName('products_title_idx') + ->setType('fulltext') + ) + ->create(); + + $this->table('special_pks', ['id' => false, 'primary_key' => ['id']]) + ->addColumn('id', 'uuid', [ + 'default' => 'a4950df3-515f-474c-be4c-6a027c1957e7', + 'limit' => null, + 'null' => false, + ]) + ->addColumn('name', 'string', [ + 'default' => null, + 'limit' => 256, + 'null' => true, + ]) + ->create(); + + $this->table('special_tags') + ->addColumn('article_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => false, + 'signed' => false, + ]) + ->addColumn('author_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'signed' => false, + ]) + ->addColumn('tag_id', 'integer', [ + 'default' => null, + 'limit' => null, + 'null' => false, + 'signed' => false, + ]) + ->addColumn('highlighted', 'boolean', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('highlighted_time', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addIndex( + $this->index('article_id') + ->setName('special_tags_article_unique') + ->setType('unique') + ) + ->create(); + + $this->table('texts', ['id' => false]) + ->addColumn('title', 'string', [ + 'default' => null, + 'limit' => 255, + 'null' => true, + ]) + ->addColumn('description', 'text', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->create(); + + $this->table('users') + ->addColumn('username', 'string', [ + 'default' => null, + 'limit' => 256, + 'null' => true, + ]) + ->addColumn('password', 'string', [ + 'default' => null, + 'limit' => 256, + 'null' => true, + ]) + ->addColumn('created', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + ]) + ->addColumn('updated', 'timestamp', [ + 'default' => null, + 'limit' => null, + 'null' => true, + 'update' => 'CURRENT_TIMESTAMP', + ]) + ->create(); + + $this->table('articles') + ->addForeignKey( + $this->foreignKey('category_id') + ->setReferencedTable('categories') + ->setReferencedColumns('id') + ->setDelete('NO_ACTION') + ->setUpdate('NO_ACTION') + ->setName('articles_category_fk') + ) + ->update(); + + $this->table('orders') + ->addForeignKey( + $this->foreignKey([ + 'product_category', + 'product_id', + ]) + ->setReferencedTable('products') + ->setReferencedColumns([ + 'category_id', + 'id', + ]) + ->setDelete('CASCADE') + ->setUpdate('CASCADE') + ->setName('orders_product_fk') + ) + ->update(); + + $this->table('products') + ->addForeignKey( + $this->foreignKey('category_id') + ->setReferencedTable('categories') + ->setReferencedColumns('id') + ->setDelete('CASCADE') + ->setUpdate('CASCADE') + ->setName('products_category_fk') + ) + ->update(); + } + + /** + * Down Method. + * + * More information on this method is available here: + * https://book.cakephp.org/migrations/5/guides/writing-migrations/migration-methods.html#the-down-method + * + * @return void + */ + public function down(): void + { + $this->table('articles') + ->dropForeignKey( + 'category_id' + )->save(); + + $this->table('orders') + ->dropForeignKey( + [ + 'product_category', + 'product_id', + ] + )->save(); + + $this->table('products') + ->dropForeignKey( + 'category_id' + )->save(); + + $this->table('articles')->drop()->save(); + $this->table('categories')->drop()->save(); + $this->table('composite_pks')->drop()->save(); + $this->table('events')->drop()->save(); + $this->table('orders')->drop()->save(); + $this->table('parts')->drop()->save(); + $this->table('products')->drop()->save(); + $this->table('special_pks')->drop()->save(); + $this->table('special_tags')->drop()->save(); + $this->table('texts')->drop()->save(); + $this->table('users')->drop()->save(); + } +} From 9c338c3db571d01f3c8d18ca25a79b580bcfc28c Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Fri, 24 Jul 2026 05:45:43 +0200 Subject: [PATCH 63/72] Fix test suite and static analysis with CakePHP 5.4 (#1108) EventDispatcherTrait is no longer generic, so the use tag annotations are reported as errors by PHPStan. Unknown subcommands are rejected before they reach EntryCommand, so the error message differs between 5.3 and 5.4. Only the parts both messages have in common are asserted. --- src/Command/MigrateCommand.php | 3 --- src/Command/ResetCommand.php | 3 --- src/Command/RollbackCommand.php | 3 --- src/Command/SeedCommand.php | 3 --- tests/TestCase/Command/EntryCommandTest.php | 5 ++++- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Command/MigrateCommand.php b/src/Command/MigrateCommand.php index a5aab5f4f..5d96eee7c 100644 --- a/src/Command/MigrateCommand.php +++ b/src/Command/MigrateCommand.php @@ -29,9 +29,6 @@ */ class MigrateCommand extends Command { - /** - * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\MigrateCommand> - */ use EventDispatcherTrait; /** diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php index a442eeaa6..b808b2bcc 100644 --- a/src/Command/ResetCommand.php +++ b/src/Command/ResetCommand.php @@ -34,9 +34,6 @@ */ class ResetCommand extends Command { - /** - * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\ResetCommand> - */ use EventDispatcherTrait; /** diff --git a/src/Command/RollbackCommand.php b/src/Command/RollbackCommand.php index 7e4ae14ec..e44548b2c 100644 --- a/src/Command/RollbackCommand.php +++ b/src/Command/RollbackCommand.php @@ -30,9 +30,6 @@ */ class RollbackCommand extends Command { - /** - * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\MigrateCommand> - */ use EventDispatcherTrait; /** diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php index 38154ea65..6274be2de 100644 --- a/src/Command/SeedCommand.php +++ b/src/Command/SeedCommand.php @@ -28,9 +28,6 @@ */ class SeedCommand extends Command { - /** - * @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\MigrateCommand> - */ use EventDispatcherTrait; /** diff --git a/tests/TestCase/Command/EntryCommandTest.php b/tests/TestCase/Command/EntryCommandTest.php index d63aed559..30175ccd3 100644 --- a/tests/TestCase/Command/EntryCommandTest.php +++ b/tests/TestCase/Command/EntryCommandTest.php @@ -56,6 +56,9 @@ public function testExecuteMissingCommand(): void $this->exec('migrations derp'); $this->assertExitError(); - $this->assertErrorContains('Could not find migrations command named `derp`'); + // CakePHP >= 5.4 rejects unknown subcommands before they reach EntryCommand, + // so only the parts both error messages have in common are asserted. + $this->assertErrorContains('derp'); + $this->assertErrorContains('--help'); } } From d40e3572940520efc880a1dbcabe263db0485513 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Mon, 27 Jul 2026 17:06:54 +0200 Subject: [PATCH 64/72] Track the plugin of executed seeds in cake_seeds (#1107) Seed classes are not namespaced, so deriving the plugin from the class name never matched and plugin seeds were logged with a null plugin. The plugin of the current run is used instead, matching how migrations are tracked. Because of the null plugin, plugin seeds were also never detected as executed, so they ran again on every seeds run and were skipped by seeds reset. Seed log entries written before this fix are still matched for plugin seeds so that they are not executed a second time. * Use instanceof check for the seed config * Assert command output in plugin seed status and reset tests --- docs/en/guides/seeding.md | 8 ++ src/Command/SeedStatusCommand.php | 15 +--- src/Db/Adapter/AbstractAdapter.php | 61 +++++++------ src/Migration/Manager.php | 15 +--- src/Util/Util.php | 42 +++++++++ tests/TestCase/Command/SeedCommandTest.php | 99 ++++++++++++++++++++++ 6 files changed, 189 insertions(+), 51 deletions(-) diff --git a/docs/en/guides/seeding.md b/docs/en/guides/seeding.md index be61f7e72..13411cd7d 100644 --- a/docs/en/guides/seeding.md +++ b/docs/en/guides/seeding.md @@ -202,6 +202,14 @@ bin/cake seeds reset > When re-running seeds with `--force`, be careful to ensure your seeds are > idempotent (safe to run multiple times) or they may create duplicate data. +Plugin seeds are tracked with their plugin name, so the `plugin` option is +required to see or reset them: + +```bash +bin/cake seeds status --plugin PluginName +bin/cake seeds reset --plugin PluginName +``` + ### Customizing the Seed Tracking Table By default, seed execution is tracked in a table named `cake_seeds`. You can diff --git a/src/Command/SeedStatusCommand.php b/src/Command/SeedStatusCommand.php index 68f983136..3a73bbd85 100644 --- a/src/Command/SeedStatusCommand.php +++ b/src/Command/SeedStatusCommand.php @@ -17,7 +17,6 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; use Cake\Console\ConsoleOptionParser; -use Cake\Core\Configure; use Migrations\Config\ConfigInterface; use Migrations\Migration\ManagerFactory; use Migrations\Util\Util; @@ -106,24 +105,14 @@ public function execute(Arguments $args, ConsoleIo $io): ?int // Build status list $statuses = []; - $appNamespace = Configure::read('App.namespace', 'App'); foreach ($seeds as $seed) { - $plugin = null; - $className = $seed::class; - - if (str_contains($className, '\\')) { - $parts = explode('\\', $className); - if (count($parts) > 1 && $parts[0] !== $appNamespace) { - $plugin = $parts[0]; - } - } - + $plugin = Util::getSeedPlugin($seed); $seedName = $seed->getName(); $executed = false; $executedAt = null; foreach ($seedLog as $entry) { - if ($entry['seed_name'] === $seedName && $entry['plugin'] === $plugin) { + if ($entry['seed_name'] === $seedName && Util::matchesSeedPlugin($entry['plugin'], $plugin)) { $executed = true; $executedAt = $entry['executed_at']; break; diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index ee76ee1a6..02034482c 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -58,6 +58,7 @@ use Migrations\Db\Table\View; use Migrations\MigrationInterface; use Migrations\SeedInterface; +use Migrations\Util\Util; use PDOException; use RuntimeException; use function Cake\Core\deprecationWarning; @@ -1096,17 +1097,7 @@ public function getSeedLog(): array */ public function seedExecuted(SeedInterface $seed, string $executedTime): AdapterInterface { - $plugin = null; - $className = $seed::class; - - if (str_contains($className, '\\')) { - $parts = explode('\\', $className); - $appNamespace = Configure::read('App.namespace', 'App'); - if (count($parts) > 1 && $parts[0] !== $appNamespace) { - $plugin = $parts[0]; - } - } - + $plugin = $this->resolveSeedPlugin($seed); $seedName = substr($seed->getName(), 0, 100); $query = $this->getInsertBuilder(); @@ -1127,31 +1118,51 @@ public function seedExecuted(SeedInterface $seed, string $executedTime): Adapter */ public function removeSeedFromLog(SeedInterface $seed): AdapterInterface { - $plugin = null; - $className = $seed::class; + $plugin = $this->resolveSeedPlugin($seed); + $seedName = $seed->getName(); - if (str_contains($className, '\\')) { - $parts = explode('\\', $className); - $appNamespace = Configure::read('App.namespace', 'App'); - if (count($parts) > 1 && $parts[0] !== $appNamespace) { - $plugin = $parts[0]; - } + $conditions = ['seed_name' => $seedName]; + if ($plugin !== null) { + // Also remove entries logged before plugin attribution was fixed. + $conditions['OR'] = [ + 'plugin' => $plugin, + 'plugin IS' => null, + ]; + } else { + $conditions['plugin IS'] = null; } - $seedName = $seed->getName(); - $query = $this->getDeleteBuilder(); $query->delete() ->from($this->getSeedSchemaTableName()) - ->where([ - 'seed_name' => $seedName, - 'plugin IS' => $plugin, - ]); + ->where($conditions); $this->executeQuery($query); return $this; } + /** + * Resolve the plugin a seed belongs to. + * + * Seed classes are not namespaced, so the plugin cannot be derived from the class + * name. The plugin of the current run is used instead, matching how migrations are + * tracked. + * + * @param \Migrations\SeedInterface $seed The seed to resolve the plugin for. + * @return string|null The plugin name or null for application seeds. + */ + protected function resolveSeedPlugin(SeedInterface $seed): ?string + { + $plugin = Util::getSeedPlugin($seed); + if ($plugin !== null) { + return $plugin; + } + + $option = $this->getOption('plugin'); + + return $option ? (string)$option : null; + } + /** * {@inheritDoc} * diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 311085d06..65ef382f4 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -10,7 +10,6 @@ use Cake\Console\Arguments; use Cake\Console\ConsoleIo; -use Cake\Core\Configure; use DateTime; use Exception; use InvalidArgumentException; @@ -214,21 +213,11 @@ public function isSeedExecuted(SeedInterface $seed): bool $seedLog = $adapter->getSeedLog(); - $plugin = null; - $className = $seed::class; - - if (str_contains($className, '\\')) { - $parts = explode('\\', $className); - $appNamespace = Configure::read('App.namespace', 'App'); - if (count($parts) > 1 && $parts[0] !== $appNamespace) { - $plugin = $parts[0]; - } - } - + $plugin = Util::getSeedPlugin($seed); $seedName = $seed->getName(); foreach ($seedLog as $entry) { - if ($entry['seed_name'] === $seedName && $entry['plugin'] === $plugin) { + if ($entry['seed_name'] === $seedName && Util::matchesSeedPlugin($entry['plugin'], $plugin)) { return true; } } diff --git a/src/Util/Util.php b/src/Util/Util.php index 25bf5e736..cd1517257 100644 --- a/src/Util/Util.php +++ b/src/Util/Util.php @@ -12,7 +12,9 @@ use Cake\Utility\Inflector; use DateTime; use DateTimeZone; +use Migrations\Config\ConfigInterface; use Migrations\Db\Adapter\UnifiedMigrationsTableStorage; +use Migrations\SeedInterface; use RuntimeException; /** @@ -208,6 +210,46 @@ public static function getSeedDisplayName(string $seedName): string return $seedName; } + /** + * Get the plugin a seed belongs to. + * + * Seed classes are not namespaced, so the plugin cannot be derived from the class + * name. The plugin of the run the seed was loaded in is used instead. + * + * @param \Migrations\SeedInterface $seed The seed to get the plugin for. + * @return string|null The plugin name, or null for application seeds. + */ + public static function getSeedPlugin(SeedInterface $seed): ?string + { + $config = $seed->getConfig(); + if (!$config instanceof ConfigInterface || !isset($config['plugin'])) { + return null; + } + + return (string)$config['plugin'] ?: null; + } + + /** + * Check whether a seed log entry belongs to the given plugin. + * + * Seeds executed before plugin attribution was fixed were logged without a plugin. + * Those entries are still matched for plugin seeds so that they are not executed twice. + * As a trade-off, an application seed sharing its name with a plugin seed can be + * matched as well, which is preferred over re-running a seed that already ran. + * + * @param string|null $entryPlugin The plugin stored in the seed log entry. + * @param string|null $plugin The plugin of the seed being checked. + * @return bool + */ + public static function matchesSeedPlugin(?string $entryPlugin, ?string $plugin): bool + { + if ($entryPlugin === $plugin) { + return true; + } + + return $plugin !== null && $entryPlugin === null; + } + /** * Expands a set of paths with curly braces (if supported by the OS). * diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php index f98e691d8..e93984102 100644 --- a/tests/TestCase/Command/SeedCommandTest.php +++ b/tests/TestCase/Command/SeedCommandTest.php @@ -497,6 +497,105 @@ public function testSeedStatusCommand(): void $this->assertOutputContains('Numbers'); } + public function testSeederPluginIsTrackedWithPluginName(): void + { + $this->_loadTestPlugin('TestBlog'); + $this->createTables(); + + $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed'); + $this->assertExitSuccess(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + $rows = $connection + ->execute('SELECT seed_name, plugin FROM cake_seeds ORDER BY seed_name') + ->fetchAll('assoc'); + + $expected = [ + ['seed_name' => 'PluginLettersSeed', 'plugin' => 'TestBlog'], + ['seed_name' => 'PluginSubLettersSeed', 'plugin' => 'TestBlog'], + ]; + $this->assertSame($expected, $rows); + } + + public function testSeederPluginLegacyLogEntryIsNotRunAgain(): void + { + $this->_loadTestPlugin('TestBlog'); + $this->createTables(); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + + // Simulate seeds executed before plugin attribution was fixed + $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed'); + $this->assertExitSuccess(); + $connection->execute('UPDATE cake_seeds SET plugin = NULL'); + + $letters = $connection->execute('SELECT COUNT(*) FROM letters'); + $this->assertEquals(4, $letters->fetchColumn(0)); + + $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed'); + $this->assertExitSuccess(); + $this->assertOutputNotContains('seeding'); + + // No additional rows were inserted by a second run + $letters = $connection->execute('SELECT COUNT(*) FROM letters'); + $this->assertEquals(4, $letters->fetchColumn(0)); + + $this->exec('seeds status -c test -p TestBlog --source CallSeeds'); + $this->assertExitSuccess(); + $this->assertOutputContains('executed'); + $this->assertOutputNotContains('pending'); + + // Resetting removes the legacy entries as well + $this->exec('seeds reset -c test -p TestBlog --source CallSeeds', ['y']); + $this->assertExitSuccess(); + + $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds'); + $this->assertEquals(0, $seedLog->fetchColumn(0)); + } + + public function testSeedStatusCommandWithPlugin(): void + { + $this->_loadTestPlugin('TestBlog'); + $this->createTables(); + + $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed'); + $this->assertExitSuccess(); + $this->assertOutputContains('seeding'); + + $this->exec('seeds status -c test -p TestBlog --source CallSeeds'); + $this->assertExitSuccess(); + $this->assertOutputContains('TestBlog'); + $this->assertOutputContains('executed'); + $this->assertOutputNotContains('pending'); + } + + public function testSeedResetCommandWithPlugin(): void + { + $this->_loadTestPlugin('TestBlog'); + $this->createTables(); + + $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed'); + $this->assertExitSuccess(); + $this->assertOutputContains('seeding'); + + $this->exec('seeds reset -c test -p TestBlog --source CallSeeds', ['y']); + $this->assertExitSuccess(); + $this->assertOutputContains('All seeds will be reset:'); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds'); + $this->assertEquals(0, $seedLog->fetchColumn(0)); + + // Verify the seed can be run again without --force + $this->exec('seeds run -c test -p TestBlog --source CallSeeds PluginLettersSeed'); + $this->assertExitSuccess(); + $this->assertOutputContains('seeding'); + $this->assertOutputNotContains('already executed'); + } + public function testSeedResetCommand(): void { $this->createTables(); From 56ec71d99c525a01fc5c9ef51d90cc69a70291d7 Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 29 Jul 2026 11:36:11 -0400 Subject: [PATCH 65/72] Add command descriptions to migration commands (#1109) This improves the output of `cake` with no parameters. --- src/Command/BakeMigrationDiffCommand.php | 8 ++++++++ src/Command/BakeMigrationSnapshotCommand.php | 8 ++++++++ src/Command/BakeSeedCommand.php | 8 ++++++++ src/Command/DumpCommand.php | 8 ++++++++ src/Command/MarkMigratedCommand.php | 8 ++++++++ src/Command/MigrateCommand.php | 8 ++++++++ src/Command/ResetCommand.php | 8 ++++++++ src/Command/RollbackCommand.php | 8 ++++++++ src/Command/SeedCommand.php | 8 ++++++++ src/Command/SeedResetCommand.php | 8 ++++++++ src/Command/StatusCommand.php | 8 ++++++++ src/Command/UpgradeCommand.php | 8 ++++++++ 12 files changed, 96 insertions(+) diff --git a/src/Command/BakeMigrationDiffCommand.php b/src/Command/BakeMigrationDiffCommand.php index b9017941f..e03c8b1eb 100644 --- a/src/Command/BakeMigrationDiffCommand.php +++ b/src/Command/BakeMigrationDiffCommand.php @@ -102,6 +102,14 @@ class BakeMigrationDiffCommand extends BakeSimpleMigrationCommand */ protected array $templateData = []; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Create a migration containing the difference between the current schema and migration state.'; + } + /** * @inheritDoc */ diff --git a/src/Command/BakeMigrationSnapshotCommand.php b/src/Command/BakeMigrationSnapshotCommand.php index 54afb2c85..375235255 100644 --- a/src/Command/BakeMigrationSnapshotCommand.php +++ b/src/Command/BakeMigrationSnapshotCommand.php @@ -37,6 +37,14 @@ class BakeMigrationSnapshotCommand extends BakeSimpleMigrationCommand protected string $_name; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Create a migration snapshot of the current schema.'; + } + /** * @inheritDoc */ diff --git a/src/Command/BakeSeedCommand.php b/src/Command/BakeSeedCommand.php index cfc2d2e35..3eed89180 100644 --- a/src/Command/BakeSeedCommand.php +++ b/src/Command/BakeSeedCommand.php @@ -42,6 +42,14 @@ class BakeSeedCommand extends SimpleBakeCommand */ protected Arguments $args; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Create a migration seed.'; + } + /** * @inheritDoc */ diff --git a/src/Command/DumpCommand.php b/src/Command/DumpCommand.php index d0469a06b..10bf3982d 100644 --- a/src/Command/DumpCommand.php +++ b/src/Command/DumpCommand.php @@ -32,6 +32,14 @@ class DumpCommand extends Command { protected string $connection; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Create a migration state dump file.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/MarkMigratedCommand.php b/src/Command/MarkMigratedCommand.php index 6e86a36c3..cbfe55014 100644 --- a/src/Command/MarkMigratedCommand.php +++ b/src/Command/MarkMigratedCommand.php @@ -26,6 +26,14 @@ */ class MarkMigratedCommand extends Command { + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Mark one or more migrations as run.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/MigrateCommand.php b/src/Command/MigrateCommand.php index 5d96eee7c..702943337 100644 --- a/src/Command/MigrateCommand.php +++ b/src/Command/MigrateCommand.php @@ -31,6 +31,14 @@ class MigrateCommand extends Command { use EventDispatcherTrait; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Run un-applied migrations.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/ResetCommand.php b/src/Command/ResetCommand.php index b808b2bcc..f5a8d021c 100644 --- a/src/Command/ResetCommand.php +++ b/src/Command/ResetCommand.php @@ -36,6 +36,14 @@ class ResetCommand extends Command { use EventDispatcherTrait; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Reset database state by dropping tables, and re-running migrations.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/RollbackCommand.php b/src/Command/RollbackCommand.php index e44548b2c..600510628 100644 --- a/src/Command/RollbackCommand.php +++ b/src/Command/RollbackCommand.php @@ -32,6 +32,14 @@ class RollbackCommand extends Command { use EventDispatcherTrait; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Rollback reversible migrations.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php index 6274be2de..a764d0643 100644 --- a/src/Command/SeedCommand.php +++ b/src/Command/SeedCommand.php @@ -30,6 +30,14 @@ class SeedCommand extends Command { use EventDispatcherTrait; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Run migration seeds.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/SeedResetCommand.php b/src/Command/SeedResetCommand.php index f35daf45a..34d5ae3fe 100644 --- a/src/Command/SeedResetCommand.php +++ b/src/Command/SeedResetCommand.php @@ -26,6 +26,14 @@ */ class SeedResetCommand extends Command { + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Reset the seed execution state.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php index 39e093125..4161d64f4 100644 --- a/src/Command/StatusCommand.php +++ b/src/Command/StatusCommand.php @@ -42,6 +42,14 @@ class StatusCommand extends Command */ public const CODE_STATUS_DOWN = 3; + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Display the state of migrations.'; + } + /** * The default name added to the application command list * diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index ba2ab65f3..76c947429 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -35,6 +35,14 @@ */ class UpgradeCommand extends Command { + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return 'Upgrade migration storage to use cake_migrations.'; + } + /** * The default name added to the application command list * From bfd684e4eec1a13afba4029936cde06112962107 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Aug 2026 23:20:02 -0400 Subject: [PATCH 66/72] Bump postcss from 8.5.16 to 8.5.25 in /docs (#1110) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.16 to 8.5.25. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.16...8.5.25) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.25 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index eab6ff358..55f0a34cc 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1642,9 +1642,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -1701,9 +1701,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "funding": [ { "type": "opencollective", @@ -1720,7 +1720,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, From 5a30a5ac7f4b7fcd652baf9a0d4ac20df68d98ac Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Mon, 17 Aug 2026 20:20:07 +0200 Subject: [PATCH 67/72] npm update for docs --- docs/package-lock.json | 1584 +++++++++++++++++----------------------- 1 file changed, 651 insertions(+), 933 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 55f0a34cc..e5011a901 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -32,12 +32,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -47,9 +47,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -70,443 +70,27 @@ } }, "node_modules/@docsearch/css": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.6.3.tgz", - "integrity": "sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.7.0.tgz", + "integrity": "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==", "license": "MIT" }, "node_modules/@docsearch/js": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.6.3.tgz", - "integrity": "sha512-qUIX2b4Apew3tv4F0qhmgShsl/Lfw4m6mqv/5/5dWNxwTcDdLMp2s3YwZ+NMGh3IKCg0pBaXm7Q5VdyU5Rj+cQ==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.7.0.tgz", + "integrity": "sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA==", "license": "MIT" }, "node_modules/@docsearch/sidepanel-js": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.6.3.tgz", - "integrity": "sha512-grGSmvXzG0if+mrzdIKykvpIAuEQ9u0sEJ2eLRRCaQfJvsWqh2C2/aY04bIzWvDh7myi5rvl8D+tUNsVrjYQ3A==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.7.0.tgz", + "integrity": "sha512-A8r34jCU8kcIk2viECEn2msA28ojUF1BLi/3v5OWWc5G2N3jOuuumBXoeYjfr8dA0UxgFSy5R2bt12dnFJQSyA==", "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.88", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.88.tgz", - "integrity": "sha512-+cvi1qCuvReL29ehi6t62L4fb7GDXe+UlGHFcsJcV7I2l9wtqn9XE2IBKcDr3CI5iGUGS5ISnXv699pSGpyx1Q==", + "version": "1.2.93", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.93.tgz", + "integrity": "sha512-/XhANjfGYOuqvSR3TmUnkQkINvQ4GVjVuukvymRbxtVFBvIq/yiXJqCDycKcQPT401OYT9H2vIY6ihAlz1QIAw==", "license": "CC0-1.0", "dependencies": { "@iconify/types": "*" @@ -524,29 +108,19 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -554,12 +128,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -567,12 +144,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -580,25 +160,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -606,79 +176,34 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "libc": [ - "glibc" ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], "libc": [ "glibc" ], @@ -686,14 +211,17 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ - "loong64" + "arm64" ], "libc": [ "musl" @@ -702,47 +230,18 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], "libc": [ "glibc" ], @@ -750,28 +249,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -782,57 +268,53 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], "libc": [ - "musl" + "glibc" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -840,12 +322,15 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -853,25 +338,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -879,90 +354,122 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" }, "node_modules/@shikijs/core": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", - "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0", + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", + "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", - "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", - "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/langs": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", - "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0" + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", - "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0" + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/transformers": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-3.23.0.tgz", - "integrity": "sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.4.3.tgz", + "integrity": "sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.23.0", - "@shikijs/types": "3.23.0" + "@shikijs/core": "4.4.3", + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", - "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/vscode-textmate": { @@ -971,16 +478,10 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -1030,15 +531,15 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", - "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", - "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", "license": "MIT", "dependencies": { "@rolldown/pluginutils": "^1.0.1" @@ -1052,141 +553,139 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", - "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.39", + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", - "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", - "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.39", - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39", + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.15", + "postcss": "^8.5.19", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", - "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/devtools-api": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz", - "integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^8.1.5" + "@vue/devtools-kit": "^8.2.1" } }, "node_modules/@vue/devtools-kit": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz", - "integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^8.1.5", + "@vue/devtools-shared": "^8.2.1", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "node_modules/@vue/devtools-shared": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz", - "integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", - "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.39" + "@vue/shared": "3.5.41" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", - "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", - "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.39", - "@vue/runtime-core": "3.5.39", - "@vue/shared": "3.5.39", + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", - "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.39", - "@vue/shared": "3.5.39" - }, - "peerDependencies": { - "vue": "3.5.39" + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" } }, "node_modules/@vue/shared": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", - "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==", "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", - "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.3.0", - "@vueuse/shared": "14.3.0" + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1196,13 +695,13 @@ } }, "node_modules/@vueuse/integrations": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.3.0.tgz", - "integrity": "sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==", + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.4.0.tgz", + "integrity": "sha512-oJz9qTgczvA7L1nXQFRU7h8tQbOCoiceqvMMhT9XYMyOGTqLJ2rEa09PON+nD2t48sZUfeOmg4eaWJXV4sZb/w==", "license": "MIT", "dependencies": { - "@vueuse/core": "14.3.0", - "@vueuse/shared": "14.3.0" + "@vueuse/core": "14.4.0", + "@vueuse/shared": "14.4.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1262,18 +761,18 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", - "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", - "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -1346,6 +845,15 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -1371,47 +879,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -1510,6 +977,267 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1642,9 +1370,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -1689,9 +1417,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { "node": ">=12" @@ -1701,9 +1429,9 @@ } }, "node_modules/postcss": { - "version": "8.5.25", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -1720,7 +1448,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1762,64 +1490,55 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/shiki": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", - "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.23.0", - "@shikijs/engine-javascript": "3.23.0", - "@shikijs/engine-oniguruma": "3.23.0", - "@shikijs/langs": "3.23.0", - "@shikijs/themes": "3.23.0", - "@shikijs/types": "3.23.0", + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/source-map-js": { @@ -1984,17 +1703,16 @@ } }, "node_modules/vite": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", - "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "license": "MIT", "dependencies": { - "esbuild": "^0.27.0 || ^0.28.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -2010,9 +1728,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -2025,13 +1744,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -2058,62 +1780,58 @@ } }, "node_modules/vitepress": { - "version": "2.0.0-alpha.17", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", - "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", + "version": "2.0.0-alpha.19", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.19.tgz", + "integrity": "sha512-WnBsb0Bwr43kXKyiis+lld/7ri3hnMbthS8N3hpFtjjwsdLO4IRmiAE08D7aud4q6oMDf9uwRowxzNqRFe/amw==", "license": "MIT", "dependencies": { - "@docsearch/css": "^4.5.3", - "@docsearch/js": "^4.5.3", - "@docsearch/sidepanel-js": "^4.5.3", - "@iconify-json/simple-icons": "^1.2.69", - "@shikijs/core": "^3.22.0", - "@shikijs/transformers": "^3.22.0", - "@shikijs/types": "^3.22.0", + "@docsearch/css": "^4.7.0", + "@docsearch/js": "^4.7.0", + "@docsearch/sidepanel-js": "^4.7.0", + "@iconify-json/simple-icons": "^1.2.92", + "@shikijs/core": "^4.4.1", + "@shikijs/transformers": "^4.4.1", + "@shikijs/types": "^4.4.1", "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^6.0.4", - "@vue/devtools-api": "^8.0.5", - "@vue/shared": "^3.5.27", - "@vueuse/core": "^14.2.0", - "@vueuse/integrations": "^14.2.0", - "focus-trap": "^8.0.0", + "@vitejs/plugin-vue": "^6.0.8", + "@vue/devtools-api": "^8.2.1", + "@vue/shared": "^3.5.40", + "@vueuse/core": "^14.4.0", + "@vueuse/integrations": "^14.4.0", + "focus-trap": "^8.2.2", "mark.js": "8.11.1", "minisearch": "^7.2.0", - "shiki": "^3.22.0", - "vite": "^7.3.1", - "vue": "^3.5.27" + "shiki": "^4.4.1", + "vite": "^8.2.0", + "vue": "^3.5.40" }, "bin": { "vitepress": "bin/vitepress.js" }, "peerDependencies": { "markdown-it-mathjax3": "^4", - "oxc-minify": "*", "postcss": "^8" }, "peerDependenciesMeta": { "markdown-it-mathjax3": { "optional": true }, - "oxc-minify": { - "optional": true - }, "postcss": { "optional": true } } }, "node_modules/vue": { - "version": "3.5.39", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", - "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "version": "3.5.41", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.39", - "@vue/compiler-sfc": "3.5.39", - "@vue/runtime-dom": "3.5.39", - "@vue/server-renderer": "3.5.39", - "@vue/shared": "3.5.39" + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" }, "peerDependencies": { "typescript": "*" From f575a7ecaafbf9cb73ef6a0b3e46d31dfa4e3bcd Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 31 Aug 2026 16:11:58 -0400 Subject: [PATCH 68/72] Pin action image and reduce permissions (#1111) Pin the image revision used for dokku deployments and reduce permissions for deploy jobs. Pinning the image revision reduces the chances of supply chain attacks on images with access to real credentials. --- .github/workflows/deploy_docs_3x.yml | 5 ++++- .github/workflows/deploy_docs_4x.yml | 5 ++++- .github/workflows/deploy_docs_5x.yml | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy_docs_3x.yml b/.github/workflows/deploy_docs_3x.yml index a22bdc7ee..dff7cc3e5 100644 --- a/.github/workflows/deploy_docs_3x.yml +++ b/.github/workflows/deploy_docs_3x.yml @@ -7,6 +7,9 @@ on: - 3.x workflow_dispatch: +permissions: + contents: read + jobs: deploy: runs-on: ubuntu-latest @@ -17,7 +20,7 @@ jobs: fetch-depth: 0 - name: Push to dokku - uses: dokku/github-action@master + uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0 with: git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-3' ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docs_4x.yml b/.github/workflows/deploy_docs_4x.yml index af3f7f76a..2cd9660f1 100644 --- a/.github/workflows/deploy_docs_4x.yml +++ b/.github/workflows/deploy_docs_4x.yml @@ -7,6 +7,9 @@ on: - 4.x workflow_dispatch: +permissions: + contents: read + jobs: deploy: runs-on: ubuntu-latest @@ -17,7 +20,7 @@ jobs: fetch-depth: 0 - name: Push to dokku - uses: dokku/github-action@master + uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0 with: git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-4' ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docs_5x.yml b/.github/workflows/deploy_docs_5x.yml index 4d186664c..96a729890 100644 --- a/.github/workflows/deploy_docs_5x.yml +++ b/.github/workflows/deploy_docs_5x.yml @@ -7,6 +7,9 @@ on: - 5.x workflow_dispatch: +permissions: + contents: read + jobs: deploy: runs-on: ubuntu-latest @@ -17,7 +20,7 @@ jobs: fetch-depth: 0 - name: Push to dokku - uses: dokku/github-action@master + uses: dokku/github-action@cc7dec1d2b9fed249a14ae462bc953bba436f78c # v1.10.0 with: git_remote_url: 'ssh://dokku@apps.cakephp.org:22/migrations-docs-5' ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }} From 27da5a511a4eeb1b6a4a5ed7d9e8ecb7c4d17437 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Wed, 16 Sep 2026 00:58:26 +0200 Subject: [PATCH 69/72] Pin Rector's PHPStan dependency (#1117) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2099ff269..864453af0 100644 --- a/composer.json +++ b/composer.json @@ -76,7 +76,7 @@ "stan-setup": "phive install", "lowest": "validate-prefer-lowest", "lowest-setup": "composer update --prefer-lowest --prefer-stable --prefer-dist --no-interaction && cp composer.json composer.backup && composer require --dev dereuromark/composer-prefer-lowest && mv composer.backup composer.json", - "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" && mv composer.backup composer.json", + "rector-setup": "cp composer.json composer.backup && composer require --dev rector/rector:\"~2.3.1\" phpstan/phpstan:\"~2.1.40\" && mv composer.backup composer.json", "rector-check": "vendor/bin/rector process --dry-run", "rector-fix": "vendor/bin/rector process", "test": "phpunit", From 2354f82b4b026eba0bfd2545fe89ae30f5ce0fd7 Mon Sep 17 00:00:00 2001 From: Mark Scherer Date: Sat, 19 Sep 2026 05:49:47 +0200 Subject: [PATCH 70/72] Group migration sets that share a migration history (#1116) Migrator::runMany() decided whether to drop tables once per migration set. A migration history is keyed by connection and plugin only, never by source, so two sets differing only in their source write to one history while each of them sees just its own directory on disk. Every set therefore reported its siblings' applied migrations as missing, and the connection was wiped on every run even when the database was already up to date. Sets are now grouped by the history they share, and a logged migration found on disk in any set of the group no longer counts as missing. testRunManyMultipleSkip relied on that spurious drop to trigger its failure, so it now forgets an applied migration to give the second run a genuine reason to drop. It no longer has to be skipped when the unified table is in use. --- src/TestSuite/Migrator.php | 67 +++++++++++++++- tests/TestCase/TestSuite/MigratorTest.php | 95 +++++++++++++++++++---- 2 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/TestSuite/Migrator.php b/src/TestSuite/Migrator.php index dd1602498..d62397c20 100644 --- a/src/TestSuite/Migrator.php +++ b/src/TestSuite/Migrator.php @@ -84,6 +84,8 @@ public function runMany( // Detect all connections involved, and mark those with changed status. $connectionsToDrop = []; $connectionsList = []; + $sets = []; + $historyGroups = []; foreach ($options as $i => $migrationSet) { $migrationSet += ['connection' => 'test']; $skip = $migrationSet['skip'] ?? []; @@ -95,9 +97,29 @@ public function runMany( $connectionsList[$connectionName] = ['name' => $connectionName, 'skip' => $skip]; } + // Sets sharing a connection and a plugin also share a migration history, + // while each of them only has its own source directory on disk. Group + // them so that a set is not told that its siblings' applied migrations + // are missing. + $groupKey = $connectionName . '|' . ($migrationSet['plugin'] ?? ''); + $historyGroups[$groupKey][] = $migrationSet; + $sets[] = ['options' => $migrationSet, 'skip' => $skip, 'group' => $groupKey]; + } + + $groupMigrationIds = []; + foreach ($historyGroups as $groupKey => $group) { + $groupMigrationIds[$groupKey] = $this->getSiblingMigrationIds($group); + } + + foreach ($sets as $set) { + $connectionName = $set['options']['connection']; + if (isset($connectionsToDrop[$connectionName])) { + continue; + } + $migrations = new Migrations(); - if (!isset($connectionsToDrop[$connectionName]) && $this->shouldDropTables($migrations, $migrationSet)) { - $connectionsToDrop[$connectionName] = ['name' => $connectionName, 'skip' => $skip]; + if ($this->shouldDropTables($migrations, $set['options'], $groupMigrationIds[$set['group']])) { + $connectionsToDrop[$connectionName] = ['name' => $connectionName, 'skip' => $set['skip']]; } } @@ -158,15 +180,48 @@ public function truncate(string $connection, array $skip = []): void } } + /** + * Collect the migration ids that exist on disk for a group of migration sets + * that share a single migration history. + * + * Returns an empty list for groups of one, where no sibling source exists. + * + * @param array> $group Migration sets sharing a history. + * @return array Migration ids present on disk, keyed by id. + */ + protected function getSiblingMigrationIds(array $group): array + { + if (count($group) < 2) { + return []; + } + + $ids = []; + foreach ($group as $options) { + $migrations = new Migrations(); + foreach ($migrations->status($options) as $migration) { + if ($migration['missing'] ?? false) { + continue; + } + $ids[$migration['id']] = true; + } + } + + return $ids; + } + /** * Detect if migrations have changed and the database needs to be wiped. * * @param \Migrations\Migrations $migrations The migrations service. * @param array $options The connection options. + * @param array $siblingMigrationIds Migration ids found in sources sharing the history. * @return bool */ - protected function shouldDropTables(Migrations $migrations, array $options): bool - { + protected function shouldDropTables( + Migrations $migrations, + array $options, + array $siblingMigrationIds = [], + ): bool { Log::write('debug', sprintf('Reading migrations status for %s...', $options['connection'])); $messages = [ @@ -175,6 +230,10 @@ protected function shouldDropTables(Migrations $migrations, array $options): boo ]; foreach ($migrations->status($options) as $migration) { if ($migration['status'] === 'up' && ($migration['missing'] ?? false)) { + // The migration belongs to another source sharing this history. + if (isset($siblingMigrationIds[$migration['id']])) { + continue; + } $messages['missing'][] = 'Applied but, missing Migration ' . sprintf('source=%s id=%s', $migration['name'], $migration['id']); } diff --git a/tests/TestCase/TestSuite/MigratorTest.php b/tests/TestCase/TestSuite/MigratorTest.php index cf2516c19..3a9804400 100644 --- a/tests/TestCase/TestSuite/MigratorTest.php +++ b/tests/TestCase/TestSuite/MigratorTest.php @@ -33,27 +33,33 @@ class MigratorTest extends TestCase protected $restore; /** - * Get the migration table name for the Migrator plugin. + * Get the migration table name for a plugin, or for the application when null. * + * @param string|null $plugin The plugin name. * @return string */ - protected function getMigratorTableName(): string + protected function getMigratorTableName(?string $plugin = 'Migrator'): string { - return Configure::read('Migrations.legacyTables') === false - ? UnifiedMigrationsTableStorage::TABLE_NAME - : 'migrator_phinxlog'; + if (Configure::read('Migrations.legacyTables') === false) { + return UnifiedMigrationsTableStorage::TABLE_NAME; + } + + return $plugin === null ? 'phinxlog' : 'migrator_phinxlog'; } /** * Build a WHERE clause for filtering by plugin in unified mode. * + * @param string|null $plugin The plugin name. * @return array */ - protected function getMigratorWhereClause(): array + protected function getMigratorWhereClause(?string $plugin = 'Migrator'): array { - return Configure::read('Migrations.legacyTables') === false - ? ['plugin' => 'Migrator'] - : []; + if (Configure::read('Migrations.legacyTables') !== false) { + return []; + } + + return $plugin === null ? ['plugin IS' => null] : ['plugin' => $plugin]; } protected function makeInspectableMigrator(): Migrator @@ -218,8 +224,6 @@ public function testRunManyMultipleSkip(): void { $connection = ConnectionManager::get('test'); $this->skipIf($connection->getDriver() instanceof Postgres); - // Skip for unified mode - migration history detection works differently - $this->skipIf(Configure::read('Migrations.legacyTables') === false); $migrator = new Migrator(); // Run migrations for the first time. @@ -228,6 +232,9 @@ public function testRunManyMultipleSkip(): void ['plugin' => 'Migrator', 'source' => 'Migrations2'], ]); + // Forget one applied migration so that the next run has a reason to drop. + $this->forgetMigration('20211001000000'); + // Run migrations the second time. Skip clauses will cause problems. try { $migrator->runMany([ @@ -253,24 +260,35 @@ public function testTruncateAfterMigrations(): void $this->assertCount(0, $connection->selectQuery()->select(['*'])->from('migrator')->execute()->fetchAll()); } - private function setMigrationEndDateToYesterday(): void + private function setMigrationEndDateToYesterday(?string $plugin = 'Migrator'): void { $query = ConnectionManager::get('test')->updateQuery() - ->update($this->getMigratorTableName()) + ->update($this->getMigratorTableName($plugin)) ->set('end_time', ChronosDate::yesterday(), 'timestamp'); - $where = $this->getMigratorWhereClause(); + $where = $this->getMigratorWhereClause($plugin); if ($where) { $query->where($where); } $query->execute(); } - private function fetchMigrationEndDate(): ChronosDate + private function forgetMigration(string $version): void + { + $query = ConnectionManager::get('test')->deleteQuery() + ->delete($this->getMigratorTableName()) + ->where(['version' => $version]); + foreach ($this->getMigratorWhereClause() as $field => $value) { + $query->where([$field => $value]); + } + $query->execute(); + } + + private function fetchMigrationEndDate(?string $plugin = 'Migrator'): ChronosDate { $query = ConnectionManager::get('test')->selectQuery() ->select('end_time') - ->from($this->getMigratorTableName()); - $where = $this->getMigratorWhereClause(); + ->from($this->getMigratorTableName($plugin)); + $where = $this->getMigratorWhereClause($plugin); if ($where) { $query->where($where); } @@ -323,6 +341,49 @@ public function testSkipMigrationDroppingIfOnlyUpMigrationsWithTwoSetsOfMigratio $this->assertTrue($this->fetchMigrationEndDate()->isYesterday()); } + public function testSkipMigrationDroppingWithTwoSourcesSharingHistory(): void + { + // Both sets resolve to the same migration history: same connection and + // same plugin. Each source only sees its own files on disk, so the + // other source's applied migrations must not be treated as missing. + $sets = [ + ['plugin' => 'Migrator'], + ['plugin' => 'Migrator', 'source' => 'Migrations2'], + ]; + + $migrator = new Migrator(); + $migrator->runMany($sets, false); + + $this->setMigrationEndDateToYesterday(); + + $migrator->runMany($sets, false); + + // Ensure that the end time is unchanged, meaning that the tables were not + // dropped and the migrations were not re-run. + $this->assertTrue($this->fetchMigrationEndDate()->isYesterday()); + } + + public function testSkipMigrationDroppingWithTwoAppSourcesSharingHistory(): void + { + // Two application sources on the same connection: no plugin, so both + // record into the same migration history. + $sets = [ + ['source' => '../Plugin/Migrator/config/Migrations'], + ['source' => '../Plugin/Migrator/config/Migrations2'], + ]; + + $migrator = new Migrator(); + $migrator->runMany($sets, false); + + $this->setMigrationEndDateToYesterday(null); + + $migrator->runMany($sets, false); + + // Ensure that the end time is unchanged, meaning that the tables were not + // dropped and the migrations were not re-run. + $this->assertTrue($this->fetchMigrationEndDate(null)->isYesterday()); + } + public function testDropMigrationsIfDownMigrations(): void { // Skip for unified mode - migration history detection works differently From f14030ef306528cb452c0248f9045e3bf6841a71 Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Tue, 6 Oct 2026 18:15:16 +0200 Subject: [PATCH 71/72] Update to PHPStan 2.3 (#1118) Co-authored-by: Kevin Pfeifer Co-authored-by: Claude Opus 5.5 (1M context) --- .phive/phars.xml | 2 +- src/Command/RollbackCommand.php | 1 - src/Db/Table.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.phive/phars.xml b/.phive/phars.xml index 848f478f2..e116501f8 100644 --- a/.phive/phars.xml +++ b/.phive/phars.xml @@ -1,4 +1,4 @@ - + diff --git a/src/Command/RollbackCommand.php b/src/Command/RollbackCommand.php index 600510628..bf45b0a7b 100644 --- a/src/Command/RollbackCommand.php +++ b/src/Command/RollbackCommand.php @@ -235,7 +235,6 @@ protected function getTargetFromDate(string|bool $date): string 4 => '0101000000', ]; - /** @var string $date */ $dateLength = strlen($date); if (!isset($dateStrlenToAppend[$dateLength])) { throw new InvalidArgumentException('Invalid date. Format is YYYY[MM[DD[HH[II[SS]]]]].'); diff --git a/src/Db/Table.php b/src/Db/Table.php index ee68e53d1..1d5487c38 100644 --- a/src/Db/Table.php +++ b/src/Db/Table.php @@ -1186,7 +1186,7 @@ protected function executeActions(bool $exists): void } // Only skip CreateTable if we have ONLY view/trigger actions (and at least one) - if (!$hasViewOrTriggerActions || $hasTableActions || $actions === []) { + if (!$hasViewOrTriggerActions || $hasTableActions) { $this->actions->addAction(new CreateTable($this->table)); } } From 9ef7b283787644c99b02b1bc4100bf31b4b244bd Mon Sep 17 00:00:00 2001 From: Jasper Smet Date: Wed, 7 Oct 2026 08:40:39 +0200 Subject: [PATCH 72/72] Fix 5.next migration dispatch analysis and snapshot expectation --- src/Migration/Environment.php | 8 ++++---- .../Migration/test_snapshot_with_on_update.php | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Migration/Environment.php b/src/Migration/Environment.php index 447ec3407..a4ddf961e 100644 --- a/src/Migration/Environment.php +++ b/src/Migration/Environment.php @@ -80,11 +80,11 @@ public function executeMigration(MigrationInterface $migration, string $directio // (statically narrowable for IDEs and static analysis), then a // method_exists fallback for migrations that haven't yet adopted // either ReversibleMigrationInterface or DirectionalMigrationInterface. - $isReversible = $migration instanceof ReversibleMigrationInterface + if ( + $migration instanceof ReversibleMigrationInterface || (!$migration instanceof DirectionalMigrationInterface - && method_exists($migration, MigrationInterface::CHANGE)); - - if ($isReversible) { + && method_exists($migration, MigrationInterface::CHANGE)) + ) { if ($direction === MigrationInterface::DOWN) { // Create an instance of the RecordingAdapter so we can record all // of the migration commands for reverse playback diff --git a/tests/comparisons/Migration/test_snapshot_with_on_update.php b/tests/comparisons/Migration/test_snapshot_with_on_update.php index a3e4c9ba4..85d284b60 100644 --- a/tests/comparisons/Migration/test_snapshot_with_on_update.php +++ b/tests/comparisons/Migration/test_snapshot_with_on_update.php @@ -2,8 +2,9 @@ declare(strict_types=1); use Migrations\BaseMigration; +use Migrations\DirectionalMigrationInterface; -class TestSnapshotWithOnUpdate extends BaseMigration +class TestSnapshotWithOnUpdate extends BaseMigration implements DirectionalMigrationInterface { /** * Up Method.