Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
282 changes: 282 additions & 0 deletions src/Command/ResetCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
<?php
declare(strict_types=1);

/**
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @license https://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Migrations\Command;

use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\Database\Connection;
use Cake\Datasource\ConnectionManager;
use Cake\Event\EventDispatcherTrait;
use Migrations\Config\ConfigInterface;
use Migrations\Db\Adapter\AdapterInterface;
use Migrations\Db\Adapter\DirectActionInterface;
use Migrations\Migration\ManagerFactory;
use RuntimeException;
use Throwable;

/**
* Reset command drops all tables and re-runs all migrations.
*
* This is a destructive operation intended for development use.
*/
class ResetCommand extends Command
{
/**
* @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\ResetCommand>
*/
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.',
'',
'<warning>This is a destructive operation!</warning>',
'All data in the database will be lost.',
'',
'<info>migrations reset</info>',
'<info>migrations reset -c secondary</info>',
'<info>migrations reset --dry-run</info>',
])->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('<warning>DRY-RUN mode enabled - no changes will be made</warning>');
$io->out('');
}

// Get tables to drop
$tablesToDrop = $this->getTablesToDrop($connection);

if (empty($tablesToDrop)) {
$io->out('<info>No tables to drop.</info>');
$io->out('');
$io->out('Running migrations...');

return $this->runMigrationsAndDispatch($args, $io);
}

// Show what will be dropped
$io->out('<warning>The following tables will be dropped:</warning>');
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',
);
Comment on lines +136 to +140
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Thanks for making this safe by default.

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<string> 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<string> $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('<info>using connection</info> ' . (string)$args->getOption('connection'));
$io->verbose('<info>using paths</info> ' . $config->getMigrationPath());

try {
$start = microtime(true);
$manager->migrate(null, false, null);
$end = microtime(true);
} catch (Throwable $e) {
$io->err('<error>' . $e->getMessage() . '</error>');
$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;
}
}
20 changes: 20 additions & 0 deletions src/Db/Adapter/AdapterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
16 changes: 16 additions & 0 deletions src/Db/Adapter/AdapterWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
16 changes: 16 additions & 0 deletions src/Db/Adapter/MysqlAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
20 changes: 19 additions & 1 deletion src/Db/Adapter/PostgresAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand All @@ -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
*/
Expand Down
Loading
Loading