Skip to content

Commit 21c1769

Browse files
authored
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
1 parent 9b190c1 commit 21c1769

File tree

11 files changed

+598
-3
lines changed

11 files changed

+598
-3
lines changed

src/Command/ResetCommand.php

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11+
* @link https://cakephp.org CakePHP(tm) Project
12+
* @license https://www.opensource.org/licenses/mit-license.php MIT License
13+
*/
14+
namespace Migrations\Command;
15+
16+
use Cake\Command\Command;
17+
use Cake\Console\Arguments;
18+
use Cake\Console\ConsoleIo;
19+
use Cake\Console\ConsoleOptionParser;
20+
use Cake\Database\Connection;
21+
use Cake\Datasource\ConnectionManager;
22+
use Cake\Event\EventDispatcherTrait;
23+
use Migrations\Config\ConfigInterface;
24+
use Migrations\Db\Adapter\AdapterInterface;
25+
use Migrations\Db\Adapter\DirectActionInterface;
26+
use Migrations\Migration\ManagerFactory;
27+
use RuntimeException;
28+
use Throwable;
29+
30+
/**
31+
* Reset command drops all tables and re-runs all migrations.
32+
*
33+
* This is a destructive operation intended for development use.
34+
*/
35+
class ResetCommand extends Command
36+
{
37+
/**
38+
* @use \Cake\Event\EventDispatcherTrait<\Migrations\Command\ResetCommand>
39+
*/
40+
use EventDispatcherTrait;
41+
42+
/**
43+
* The default name added to the application command list
44+
*
45+
* @return string
46+
*/
47+
public static function defaultName(): string
48+
{
49+
return 'migrations reset';
50+
}
51+
52+
/**
53+
* Configure the option parser
54+
*
55+
* @param \Cake\Console\ConsoleOptionParser $parser The option parser to configure
56+
* @return \Cake\Console\ConsoleOptionParser
57+
*/
58+
public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
59+
{
60+
$parser->setDescription([
61+
'Drop all tables and re-run all migrations.',
62+
'',
63+
'<warning>This is a destructive operation!</warning>',
64+
'All data in the database will be lost.',
65+
'',
66+
'<info>migrations reset</info>',
67+
'<info>migrations reset -c secondary</info>',
68+
'<info>migrations reset --dry-run</info>',
69+
])->addOption('plugin', [
70+
'short' => 'p',
71+
'help' => 'The plugin to run migrations for',
72+
])->addOption('connection', [
73+
'short' => 'c',
74+
'help' => 'The datasource connection to use',
75+
'default' => 'default',
76+
])->addOption('source', [
77+
'short' => 's',
78+
'default' => ConfigInterface::DEFAULT_MIGRATION_FOLDER,
79+
'help' => 'The folder where your migrations are',
80+
])->addOption('dry-run', [
81+
'short' => 'x',
82+
'help' => 'Preview what tables would be dropped without making changes',
83+
'boolean' => true,
84+
])->addOption('no-lock', [
85+
'help' => 'If present, no lock file will be generated after migrating',
86+
'boolean' => true,
87+
]);
88+
89+
return $parser;
90+
}
91+
92+
/**
93+
* Execute the command.
94+
*
95+
* @param \Cake\Console\Arguments $args The command arguments.
96+
* @param \Cake\Console\ConsoleIo $io The console io
97+
* @return int|null The exit code or null for success
98+
*/
99+
public function execute(Arguments $args, ConsoleIo $io): ?int
100+
{
101+
$event = $this->dispatchEvent('Migration.beforeReset');
102+
if ($event->isStopped()) {
103+
return $event->getResult() ? self::CODE_SUCCESS : self::CODE_ERROR;
104+
}
105+
106+
$connectionName = (string)$args->getOption('connection');
107+
/** @var \Cake\Database\Connection $connection */
108+
$connection = ConnectionManager::get($connectionName);
109+
$dryRun = (bool)$args->getOption('dry-run');
110+
111+
if ($dryRun) {
112+
$io->out('<warning>DRY-RUN mode enabled - no changes will be made</warning>');
113+
$io->out('');
114+
}
115+
116+
// Get tables to drop
117+
$tablesToDrop = $this->getTablesToDrop($connection);
118+
119+
if (empty($tablesToDrop)) {
120+
$io->out('<info>No tables to drop.</info>');
121+
$io->out('');
122+
$io->out('Running migrations...');
123+
124+
return $this->runMigrationsAndDispatch($args, $io);
125+
}
126+
127+
// Show what will be dropped
128+
$io->out('<warning>The following tables will be dropped:</warning>');
129+
foreach ($tablesToDrop as $table) {
130+
$io->out(' - ' . $table);
131+
}
132+
$io->out('');
133+
134+
// Ask for confirmation (unless dry-run)
135+
if (!$dryRun) {
136+
$continue = $io->askChoice(
137+
'This will permanently delete all data. Do you want to continue?',
138+
['y', 'n'],
139+
'n',
140+
);
141+
if ($continue !== 'y') {
142+
$io->warning('Reset operation aborted.');
143+
144+
return self::CODE_SUCCESS;
145+
}
146+
}
147+
148+
// Drop tables
149+
$io->out('');
150+
if (!$dryRun) {
151+
$factory = new ManagerFactory([
152+
'plugin' => $args->getOption('plugin'),
153+
'source' => $args->getOption('source'),
154+
'connection' => $args->getOption('connection'),
155+
]);
156+
$manager = $factory->createManager($io);
157+
$adapter = $manager->getEnvironment()->getAdapter();
158+
159+
$this->dropTables($adapter, $tablesToDrop, $io);
160+
} else {
161+
$io->info('DRY-RUN: Would drop ' . count($tablesToDrop) . ' table(s).');
162+
}
163+
164+
$io->out('');
165+
166+
// Re-run migrations
167+
if (!$dryRun) {
168+
return $this->runMigrationsAndDispatch($args, $io);
169+
}
170+
171+
$io->info('DRY-RUN: Would re-run all migrations.');
172+
173+
return self::CODE_SUCCESS;
174+
}
175+
176+
/**
177+
* Get list of tables to drop.
178+
*
179+
* @param \Cake\Database\Connection $connection Database connection
180+
* @return array<string> List of table names
181+
*/
182+
protected function getTablesToDrop(Connection $connection): array
183+
{
184+
$schema = $connection->getDriver()->schemaDialect();
185+
186+
return $schema->listTables();
187+
}
188+
189+
/**
190+
* Drop tables with foreign key handling.
191+
*
192+
* @param \Migrations\Db\Adapter\AdapterInterface $adapter The adapter
193+
* @param array<string> $tables Tables to drop
194+
* @param \Cake\Console\ConsoleIo $io Console IO
195+
* @return void
196+
*/
197+
protected function dropTables(AdapterInterface $adapter, array $tables, ConsoleIo $io): void
198+
{
199+
if (!$adapter instanceof DirectActionInterface) {
200+
throw new RuntimeException('The adapter must implement DirectActionInterface');
201+
}
202+
203+
$adapter->disableForeignKeyConstraints();
204+
205+
try {
206+
foreach ($tables as $table) {
207+
$io->verbose("Dropping table: {$table}");
208+
$adapter->dropTable($table);
209+
}
210+
} finally {
211+
$adapter->enableForeignKeyConstraints();
212+
}
213+
214+
$io->success('Dropped ' . count($tables) . ' table(s).');
215+
}
216+
217+
/**
218+
* Run migrations and dispatch afterReset event.
219+
*
220+
* @param \Cake\Console\Arguments $args The command arguments.
221+
* @param \Cake\Console\ConsoleIo $io The console io
222+
* @return int|null The exit code
223+
*/
224+
protected function runMigrationsAndDispatch(Arguments $args, ConsoleIo $io): ?int
225+
{
226+
$result = $this->runMigrations($args, $io);
227+
$this->dispatchEvent('Migration.afterReset');
228+
229+
return $result;
230+
}
231+
232+
/**
233+
* Run migrations.
234+
*
235+
* @param \Cake\Console\Arguments $args The command arguments.
236+
* @param \Cake\Console\ConsoleIo $io The console io
237+
* @return int|null The exit code
238+
*/
239+
protected function runMigrations(Arguments $args, ConsoleIo $io): ?int
240+
{
241+
$factory = new ManagerFactory([
242+
'plugin' => $args->getOption('plugin'),
243+
'source' => $args->getOption('source'),
244+
'connection' => $args->getOption('connection'),
245+
'dry-run' => (bool)$args->getOption('dry-run'),
246+
]);
247+
248+
$manager = $factory->createManager($io);
249+
$config = $manager->getConfig();
250+
251+
$io->verbose('<info>using connection</info> ' . (string)$args->getOption('connection'));
252+
$io->verbose('<info>using paths</info> ' . $config->getMigrationPath());
253+
254+
try {
255+
$start = microtime(true);
256+
$manager->migrate(null, false, null);
257+
$end = microtime(true);
258+
} catch (Throwable $e) {
259+
$io->err('<error>' . $e->getMessage() . '</error>');
260+
$io->verbose($e->getTraceAsString());
261+
262+
return self::CODE_ERROR;
263+
}
264+
265+
$io->comment('All Done. Took ' . sprintf('%.4fs', $end - $start));
266+
$io->out('');
267+
268+
$exitCode = self::CODE_SUCCESS;
269+
270+
// Run dump command to generate lock file
271+
if (!$args->getOption('no-lock') && !$args->getOption('dry-run')) {
272+
$io->verbose('');
273+
$io->verbose('Dumping the current schema of the database to be used while baking a diff');
274+
$io->verbose('');
275+
276+
$newArgs = DumpCommand::extractArgs($args);
277+
$exitCode = $this->executeCommand(DumpCommand::class, $newArgs, $io);
278+
}
279+
280+
return $exitCode;
281+
}
282+
}

src/Db/Adapter/AdapterInterface.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,26 @@ public function createTable(TableMetadata $table, array $columns = [], array $in
553553
*/
554554
public function truncateTable(string $tableName): void;
555555

556+
/**
557+
* Disable foreign key constraint checking.
558+
*
559+
* This is useful when dropping tables or performing bulk operations
560+
* that would otherwise fail due to foreign key constraints.
561+
*
562+
* @return void
563+
*/
564+
public function disableForeignKeyConstraints(): void;
565+
566+
/**
567+
* Enable foreign key constraint checking.
568+
*
569+
* This should be called after disableForeignKeyConstraints() to
570+
* restore normal constraint checking behavior.
571+
*
572+
* @return void
573+
*/
574+
public function enableForeignKeyConstraints(): void;
575+
556576
/**
557577
* Returns table columns
558578
*

src/Db/Adapter/AdapterWrapper.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,4 +590,20 @@ public function getSchemaTableName(): string
590590
{
591591
return $this->getAdapter()->getSchemaTableName();
592592
}
593+
594+
/**
595+
* @inheritDoc
596+
*/
597+
public function disableForeignKeyConstraints(): void
598+
{
599+
$this->getAdapter()->disableForeignKeyConstraints();
600+
}
601+
602+
/**
603+
* @inheritDoc
604+
*/
605+
public function enableForeignKeyConstraints(): void
606+
{
607+
$this->getAdapter()->enableForeignKeyConstraints();
608+
}
593609
}

src/Db/Adapter/MysqlAdapter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,22 @@ public function truncateTable(string $tableName): void
560560
$this->execute($sql);
561561
}
562562

563+
/**
564+
* @inheritDoc
565+
*/
566+
public function disableForeignKeyConstraints(): void
567+
{
568+
$this->execute('SET FOREIGN_KEY_CHECKS = 0');
569+
}
570+
571+
/**
572+
* @inheritDoc
573+
*/
574+
public function enableForeignKeyConstraints(): void
575+
{
576+
$this->execute('SET FOREIGN_KEY_CHECKS = 1');
577+
}
578+
563579
/**
564580
* Convert from cakephp/database conventions to migrations\column
565581
*

src/Db/Adapter/PostgresAdapter.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ protected function getRenameTableInstructions(string $tableName, string $newTabl
333333
protected function getDropTableInstructions(string $tableName): AlterInstructions
334334
{
335335
$this->removeCreatedTable($tableName);
336-
$sql = sprintf('DROP TABLE %s', $this->quoteTableName($tableName));
336+
$sql = sprintf('DROP TABLE %s CASCADE', $this->quoteTableName($tableName));
337337

338338
return new AlterInstructions([], [$sql]);
339339
}
@@ -351,6 +351,24 @@ public function truncateTable(string $tableName): void
351351
$this->execute($sql);
352352
}
353353

354+
/**
355+
* @inheritDoc
356+
*/
357+
public function disableForeignKeyConstraints(): void
358+
{
359+
// PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks.
360+
// This method is a no-op for PostgreSQL since dropTable already uses CASCADE.
361+
}
362+
363+
/**
364+
* @inheritDoc
365+
*/
366+
public function enableForeignKeyConstraints(): void
367+
{
368+
// PostgreSQL uses CASCADE on DROP TABLE instead of disabling FK checks.
369+
// This method is a no-op for PostgreSQL.
370+
}
371+
354372
/**
355373
* @inheritDoc
356374
*/

0 commit comments

Comments
 (0)