Skip to content

Commit 58e669c

Browse files
authored
Add --fake flag to seeds run and --seed option to seeds reset (#985)
- Add --fake flag to mark seeds as executed without running them - Add --seed option to seeds reset for selective seed reset - Both features mirror similar functionality in migrations * Show clear message when faking idempotent seeds Idempotent seeds are not tracked, so faking them doesn't make sense. Instead of showing misleading "faking"/"faked" messages, now shows "skipped (idempotent)" to clarify what's happening.
1 parent 653da16 commit 58e669c

File tree

4 files changed

+206
-15
lines changed

4 files changed

+206
-15
lines changed

src/Command/SeedCommand.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
9393
'short' => 'f',
9494
'help' => 'Force re-running seeds that have already been executed',
9595
'boolean' => true,
96+
])
97+
->addOption('fake', [
98+
'help' => 'Mark seeds as executed without actually running them',
99+
'boolean' => true,
96100
]);
97101

98102
return $parser;
@@ -154,9 +158,14 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
154158

155159
$versionOrder = $config->getVersionOrder();
156160

161+
$fake = (bool)$args->getOption('fake');
162+
157163
if ($config->isDryRun()) {
158164
$io->info('DRY-RUN mode enabled');
159165
}
166+
if ($fake) {
167+
$io->warning('performing fake seeding');
168+
}
160169
$io->verbose('<info>using connection</info> ' . (string)$args->getOption('connection'));
161170
$io->verbose('<info>using paths</info> ' . $config->getMigrationPath());
162171
$io->verbose('<info>ordering by</info> ' . $versionOrder . ' time');
@@ -206,11 +215,11 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
206215
}
207216

208217
// run all the seed(ers)
209-
$manager->seed(null, (bool)$args->getOption('force'));
218+
$manager->seed(null, (bool)$args->getOption('force'), $fake);
210219
} else {
211220
// run seed(ers) specified as arguments
212221
foreach ($seeds as $seed) {
213-
$manager->seed(trim($seed), (bool)$args->getOption('force'));
222+
$manager->seed(trim($seed), (bool)$args->getOption('force'), $fake);
214223
}
215224
}
216225
$end = microtime(true);

src/Command/SeedResetCommand.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
4949
'allowing seeds to be re-run without the --force flag.',
5050
'',
5151
'<info>seeds reset</info>',
52+
'<info>seeds reset --seed Users</info>',
53+
'<info>seeds reset --seed Users,Posts</info>',
5254
'<info>seeds reset --plugin Demo</info>',
5355
'<info>seeds reset -c secondary</info>',
56+
])->addOption('seed', [
57+
'help' => 'Comma-separated list of specific seeds to reset. Resets all seeds if not specified.',
5458
])->addOption('plugin', [
5559
'short' => 'p',
5660
'help' => 'The plugin to reset seeds for',
@@ -100,9 +104,25 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
100104
$seeds = $manager->getSeeds();
101105
$adapter = $manager->getEnvironment()->getAdapter();
102106

103-
// Reset all seeds
107+
// Filter seeds if --seed option is specified
108+
$seedOption = $args->getOption('seed');
104109
$seedsToReset = $seeds;
105110

111+
if ($seedOption) {
112+
$requestedSeeds = array_map('trim', explode(',', (string)$seedOption));
113+
$seedsToReset = [];
114+
115+
foreach ($requestedSeeds as $requestedSeed) {
116+
$normalizedName = $manager->normalizeSeedName($requestedSeed, $seeds);
117+
if ($normalizedName === null) {
118+
$io->error("Seed `{$requestedSeed}` does not exist.");
119+
120+
return self::CODE_ERROR;
121+
}
122+
$seedsToReset[$normalizedName] = $seeds[$normalizedName];
123+
}
124+
}
125+
106126
if (empty($seedsToReset)) {
107127
$io->warning('No seeds to reset.');
108128

@@ -111,7 +131,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
111131

112132
// Show what will be reset and ask for confirmation
113133
$io->out('');
114-
$io->out('<info>All seeds will be reset:</info>');
134+
$resetAllMessage = $seedOption ? '<info>The following seeds will be reset:</info>' : '<info>All seeds will be reset:</info>';
135+
$io->out($resetAllMessage);
115136
foreach ($seedsToReset as $seed) {
116137
$io->out(' - ' . Util::getSeedDisplayName($seed->getName()));
117138
}

src/Migration/Manager.php

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -540,9 +540,10 @@ public function executeMigration(MigrationInterface $migration, string $directio
540540
*
541541
* @param \Migrations\SeedInterface $seed Seed
542542
* @param bool $force Force re-execution even if seed has already been executed
543+
* @param bool $fake Record seed as executed without actually running it
543544
* @return void
544545
*/
545-
public function executeSeed(SeedInterface $seed, bool $force = false): void
546+
public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake = false): void
546547
{
547548
$this->getIo()->out('');
548549

@@ -560,6 +561,31 @@ public function executeSeed(SeedInterface $seed, bool $force = false): void
560561
return;
561562
}
562563

564+
// Ensure seed schema table exists
565+
$adapter = $this->getEnvironment()->getAdapter();
566+
if (!$adapter->hasTable($adapter->getSeedSchemaTableName())) {
567+
$adapter->createSeedSchemaTable();
568+
}
569+
570+
if ($fake) {
571+
// Idempotent seeds are not tracked, so faking doesn't apply
572+
if ($seed->isIdempotent()) {
573+
$this->printSeedStatus($seed, 'skipped (idempotent)');
574+
575+
return;
576+
}
577+
578+
// Record seed as executed without running it
579+
$this->printSeedStatus($seed, 'faking');
580+
581+
$executedTime = date('Y-m-d H:i:s');
582+
$adapter->seedExecuted($seed, $executedTime);
583+
584+
$this->printSeedStatus($seed, 'faked');
585+
586+
return;
587+
}
588+
563589
// Auto-execute missing dependencies
564590
$missingDeps = $this->getSeedDependenciesNotExecuted($seed);
565591
if (!empty($missingDeps)) {
@@ -568,18 +594,12 @@ public function executeSeed(SeedInterface $seed, bool $force = false): void
568594
' Auto-executing dependency: %s',
569595
$depSeed->getName(),
570596
));
571-
$this->executeSeed($depSeed, $force);
597+
$this->executeSeed($depSeed, $force, $fake);
572598
}
573599
}
574600

575601
$this->printSeedStatus($seed, 'seeding');
576602

577-
// Ensure seed schema table exists
578-
$adapter = $this->getEnvironment()->getAdapter();
579-
if (!$adapter->hasTable($adapter->getSeedSchemaTableName())) {
580-
$adapter->createSeedSchemaTable();
581-
}
582-
583603
// Execute the seeder and log the time elapsed.
584604
$start = microtime(true);
585605
$this->getEnvironment()->executeSeed($seed);
@@ -794,25 +814,26 @@ public function rollback(int|string|null $target = null, bool $force = false, bo
794814
*
795815
* @param string|null $seed Seeder
796816
* @param bool $force Force re-execution even if seed has already been executed
817+
* @param bool $fake Record seed as executed without actually running it
797818
* @throws \InvalidArgumentException
798819
* @return void
799820
*/
800-
public function seed(?string $seed = null, bool $force = false): void
821+
public function seed(?string $seed = null, bool $force = false, bool $fake = false): void
801822
{
802823
$seeds = $this->getSeeds();
803824

804825
if ($seed === null) {
805826
// run all seeders
806827
foreach ($seeds as $seeder) {
807828
if (array_key_exists($seeder->getName(), $seeds)) {
808-
$this->executeSeed($seeder, $force);
829+
$this->executeSeed($seeder, $force, $fake);
809830
}
810831
}
811832
} else {
812833
// run only one seeder
813834
$normalizedName = $this->normalizeSeedName($seed, $seeds);
814835
if ($normalizedName !== null) {
815-
$this->executeSeed($seeds[$normalizedName], $force);
836+
$this->executeSeed($seeds[$normalizedName], $force, $fake);
816837
} else {
817838
throw new InvalidArgumentException(sprintf('The seed `%s` does not exist', $seed));
818839
}

tests/TestCase/Command/SeedCommandTest.php

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,4 +570,144 @@ public function testNonIdempotentSeedIsTracked(): void
570570
$this->assertOutputContains('already executed');
571571
$this->assertOutputNotContains('seeding');
572572
}
573+
574+
public function testFakeSeedMarksAsExecuted(): void
575+
{
576+
$this->createTables();
577+
578+
/** @var \Cake\Database\Connection $connection */
579+
$connection = ConnectionManager::get('test');
580+
581+
// Run with --fake flag
582+
$this->exec('seeds run -c test NumbersSeed --fake');
583+
$this->assertExitSuccess();
584+
$this->assertErrorContains('performing fake seeding');
585+
$this->assertOutputContains('faking');
586+
$this->assertOutputContains('faked');
587+
$this->assertOutputNotContains('seeding');
588+
589+
// Verify NO data was inserted
590+
$query = $connection->execute('SELECT COUNT(*) FROM numbers');
591+
$this->assertEquals(0, $query->fetchColumn(0), 'Fake seed should not insert data');
592+
593+
// Verify the seed WAS tracked in cake_seeds table
594+
$seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
595+
$this->assertEquals(1, $seedLog->fetchColumn(0), 'Fake seeds should be tracked');
596+
597+
// Running again should show already executed
598+
$this->exec('seeds run -c test NumbersSeed');
599+
$this->assertExitSuccess();
600+
$this->assertOutputContains('already executed');
601+
}
602+
603+
public function testFakeSeedWithForce(): void
604+
{
605+
$this->createTables();
606+
607+
/** @var \Cake\Database\Connection $connection */
608+
$connection = ConnectionManager::get('test');
609+
610+
// Run with --fake first
611+
$this->exec('seeds run -c test NumbersSeed --fake');
612+
$this->assertExitSuccess();
613+
614+
// Verify seed is tracked
615+
$seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
616+
$this->assertEquals(1, $seedLog->fetchColumn(0));
617+
618+
// Run with --force to actually execute it
619+
$this->exec('seeds run -c test NumbersSeed --force');
620+
$this->assertExitSuccess();
621+
$this->assertOutputContains('seeding');
622+
623+
// Verify data was inserted
624+
$query = $connection->execute('SELECT COUNT(*) FROM numbers');
625+
$this->assertEquals(1, $query->fetchColumn(0));
626+
}
627+
628+
public function testResetSpecificSeed(): void
629+
{
630+
$this->createTables();
631+
632+
/** @var \Cake\Database\Connection $connection */
633+
$connection = ConnectionManager::get('test');
634+
635+
// Run two seeds
636+
$this->exec('seeds run -c test NumbersSeed');
637+
$this->assertExitSuccess();
638+
639+
$this->exec('seeds run -c test StoresSeed');
640+
$this->assertExitSuccess();
641+
642+
// Verify both are tracked
643+
$numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
644+
$this->assertEquals(1, $numbersLog->fetchColumn(0));
645+
646+
$storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
647+
$this->assertEquals(1, $storesLog->fetchColumn(0));
648+
649+
// Reset only Numbers seed
650+
$this->exec('seeds reset -c test --seed Numbers', ['y']);
651+
$this->assertExitSuccess();
652+
$this->assertOutputContains('The following seeds will be reset:');
653+
$this->assertOutputNotContains('All seeds will be reset:');
654+
655+
// Verify Numbers is reset but Stores is still tracked
656+
$numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
657+
$this->assertEquals(0, $numbersLog->fetchColumn(0), 'Numbers seed should be reset');
658+
659+
$storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
660+
$this->assertEquals(1, $storesLog->fetchColumn(0), 'Stores seed should still be tracked');
661+
}
662+
663+
public function testResetMultipleSpecificSeeds(): void
664+
{
665+
$this->createTables();
666+
667+
/** @var \Cake\Database\Connection $connection */
668+
$connection = ConnectionManager::get('test');
669+
670+
// Run seeds
671+
$this->exec('seeds run -c test NumbersSeed');
672+
$this->exec('seeds run -c test StoresSeed');
673+
674+
// Reset both with comma-separated list
675+
$this->exec('seeds reset -c test --seed Numbers,Stores', ['y']);
676+
$this->assertExitSuccess();
677+
678+
// Verify both are reset
679+
$numbersLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'NumbersSeed\'');
680+
$this->assertEquals(0, $numbersLog->fetchColumn(0));
681+
682+
$storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
683+
$this->assertEquals(0, $storesLog->fetchColumn(0));
684+
}
685+
686+
public function testResetNonExistentSeed(): void
687+
{
688+
$this->createTables();
689+
690+
$this->exec('seeds reset -c test --seed NonExistent');
691+
$this->assertExitError();
692+
$this->assertErrorContains('Seed `NonExistent` does not exist');
693+
}
694+
695+
public function testFakeIdempotentSeedIsSkipped(): void
696+
{
697+
$this->createTables();
698+
699+
/** @var \Cake\Database\Connection $connection */
700+
$connection = ConnectionManager::get('test');
701+
702+
// Run idempotent seed with --fake flag
703+
$this->exec('seeds run -c test -s TestSeeds IdempotentTest --fake');
704+
$this->assertExitSuccess();
705+
$this->assertOutputContains('skipped (idempotent)');
706+
$this->assertOutputNotContains('faking');
707+
$this->assertOutputNotContains('faked');
708+
709+
// Verify the seed was NOT tracked (idempotent seeds are never tracked)
710+
$seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\'');
711+
$this->assertEquals(0, $seedLog->fetchColumn(0), 'Idempotent seeds should not be tracked even when faked');
712+
}
573713
}

0 commit comments

Comments
 (0)