From 2a4162375316a98010a0d31f2243dba775dac6d4 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sat, 7 Mar 2026 19:17:45 +0100 Subject: [PATCH 1/2] 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 --- src/Command/UpgradeCommand.php | 34 ++++++++++- tests/TestCase/Command/UpgradeCommandTest.php | 60 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index 5897c458..c87507e9 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; @@ -181,13 +182,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; } } @@ -195,6 +207,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 ceddd953..ba369a0e 100644 --- a/tests/TestCase/Command/UpgradeCommandTest.php +++ b/tests/TestCase/Command/UpgradeCommandTest.php @@ -166,4 +166,64 @@ 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); + + /** @var \Cake\Database\Connection $connection */ + $connection = ConnectionManager::get('test'); + $driver = $connection->getDriver(); + + // Create the plugin's phinxlog table (cake_d_c_users_phinxlog) + $tableName = $driver->quoteIdentifier('cake_d_c_users_phinxlog'); + $connection->execute("DROP TABLE IF EXISTS {$tableName}"); + $connection->execute("CREATE TABLE {$tableName} ( + version BIGINT NOT NULL PRIMARY KEY, + migration_name VARCHAR(100) DEFAULT NULL, + start_time TIMESTAMP NULL, + end_time TIMESTAMP NULL, + breakpoint BOOLEAN NOT NULL DEFAULT false + )"); + + // Insert a migration record + $connection->insertQuery() + ->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 + $connection->execute("DROP TABLE IF EXISTS {$tableName}"); + $this->removePlugins(['CakeDC/Users']); + } + } } From 352ecbd9047cfbb463aaf8ee3eb992853c758403 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sat, 7 Mar 2026 19:22:15 +0100 Subject: [PATCH 2/2] Fix test for SQL Server compatibility --- tests/TestCase/Command/UpgradeCommandTest.php | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/TestCase/Command/UpgradeCommandTest.php b/tests/TestCase/Command/UpgradeCommandTest.php index ba369a0e..c7900bd7 100644 --- a/tests/TestCase/Command/UpgradeCommandTest.php +++ b/tests/TestCase/Command/UpgradeCommandTest.php @@ -175,23 +175,22 @@ public function testExecuteWithSlashInPluginName(): void { Configure::write('Migrations.legacyTables', true); - /** @var \Cake\Database\Connection $connection */ - $connection = ConnectionManager::get('test'); - $driver = $connection->getDriver(); - - // Create the plugin's phinxlog table (cake_d_c_users_phinxlog) - $tableName = $driver->quoteIdentifier('cake_d_c_users_phinxlog'); - $connection->execute("DROP TABLE IF EXISTS {$tableName}"); - $connection->execute("CREATE TABLE {$tableName} ( - version BIGINT NOT NULL PRIMARY KEY, - migration_name VARCHAR(100) DEFAULT NULL, - start_time TIMESTAMP NULL, - end_time TIMESTAMP NULL, - breakpoint BOOLEAN NOT NULL DEFAULT false - )"); + // 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 - $connection->insertQuery() + $adapter->getInsertBuilder() ->insert(['version', 'migration_name', 'breakpoint']) ->into('cake_d_c_users_phinxlog') ->values([ @@ -222,7 +221,9 @@ public function testExecuteWithSlashInPluginName(): void $this->assertSame('CakeDC/Users', $rows[0]['plugin']); } finally { // Cleanup - $connection->execute("DROP TABLE IF EXISTS {$tableName}"); + /** @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']); } }