diff --git a/src/Command/SeedCommand.php b/src/Command/SeedCommand.php
index b7562809..88e233b0 100644
--- a/src/Command/SeedCommand.php
+++ b/src/Command/SeedCommand.php
@@ -93,6 +93,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'short' => 'f',
'help' => 'Force re-running seeds that have already been executed',
'boolean' => true,
+ ])
+ ->addOption('fake', [
+ 'help' => 'Mark seeds as executed without actually running them',
+ 'boolean' => true,
]);
return $parser;
@@ -154,9 +158,14 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
$versionOrder = $config->getVersionOrder();
+ $fake = (bool)$args->getOption('fake');
+
if ($config->isDryRun()) {
$io->info('DRY-RUN mode enabled');
}
+ if ($fake) {
+ $io->warning('performing fake seeding');
+ }
$io->verbose('using connection ' . (string)$args->getOption('connection'));
$io->verbose('using paths ' . $config->getMigrationPath());
$io->verbose('ordering by ' . $versionOrder . ' time');
@@ -205,11 +214,11 @@ protected function executeSeeds(Arguments $args, ConsoleIo $io): ?int
}
// run all the seed(ers)
- $manager->seed(null, (bool)$args->getOption('force'));
+ $manager->seed(null, (bool)$args->getOption('force'), $fake);
} else {
// run seed(ers) specified as arguments
foreach ($seeds as $seed) {
- $manager->seed(trim($seed), (bool)$args->getOption('force'));
+ $manager->seed(trim($seed), (bool)$args->getOption('force'), $fake);
}
}
$end = microtime(true);
diff --git a/src/Command/SeedResetCommand.php b/src/Command/SeedResetCommand.php
index 18ef30b1..f11964d0 100644
--- a/src/Command/SeedResetCommand.php
+++ b/src/Command/SeedResetCommand.php
@@ -49,8 +49,12 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar
'allowing seeds to be re-run without the --force flag.',
'',
'seeds reset',
+ 'seeds reset --seed Users',
+ 'seeds reset --seed Users,Posts',
'seeds reset --plugin Demo',
'seeds reset -c secondary',
+ ])->addOption('seed', [
+ 'help' => 'Comma-separated list of specific seeds to reset. Resets all seeds if not specified.',
])->addOption('plugin', [
'short' => 'p',
'help' => 'The plugin to reset seeds for',
@@ -100,9 +104,25 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
$seeds = $manager->getSeeds();
$adapter = $manager->getEnvironment()->getAdapter();
- // Reset all seeds
+ // Filter seeds if --seed option is specified
+ $seedOption = $args->getOption('seed');
$seedsToReset = $seeds;
+ if ($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.");
+
+ return self::CODE_ERROR;
+ }
+ $seedsToReset[$normalizedName] = $seeds[$normalizedName];
+ }
+ }
+
if (empty($seedsToReset)) {
$io->warning('No seeds to reset.');
@@ -111,7 +131,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int
// Show what will be reset and ask for confirmation
$io->out('');
- $io->out('All seeds will be reset:');
+ $resetAllMessage = $seedOption ? 'The following seeds will be reset:' : 'All seeds will be reset:';
+ $io->out($resetAllMessage);
foreach ($seedsToReset as $seed) {
$io->out(' - ' . Util::getSeedDisplayName($seed->getName()));
}
diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php
index 963bd57f..2ce00a17 100644
--- a/src/Migration/Manager.php
+++ b/src/Migration/Manager.php
@@ -540,9 +540,10 @@ public function executeMigration(MigrationInterface $migration, string $directio
*
* @param \Migrations\SeedInterface $seed Seed
* @param bool $force Force re-execution even if seed has already been executed
+ * @param bool $fake Record seed as executed without actually running it
* @return void
*/
- public function executeSeed(SeedInterface $seed, bool $force = false): void
+ public function executeSeed(SeedInterface $seed, bool $force = false, bool $fake = false): void
{
$this->getIo()->out('');
@@ -560,6 +561,31 @@ public function executeSeed(SeedInterface $seed, bool $force = false): void
return;
}
+ // Ensure seed schema table exists
+ $adapter = $this->getEnvironment()->getAdapter();
+ if (!$adapter->hasTable($adapter->getSeedSchemaTableName())) {
+ $adapter->createSeedSchemaTable();
+ }
+
+ if ($fake) {
+ // Idempotent seeds are not tracked, so faking doesn't apply
+ if ($seed->isIdempotent()) {
+ $this->printSeedStatus($seed, 'skipped (idempotent)');
+
+ return;
+ }
+
+ // Record seed as executed without running it
+ $this->printSeedStatus($seed, 'faking');
+
+ $executedTime = date('Y-m-d H:i:s');
+ $adapter->seedExecuted($seed, $executedTime);
+
+ $this->printSeedStatus($seed, 'faked');
+
+ return;
+ }
+
// Auto-execute missing dependencies
$missingDeps = $this->getSeedDependenciesNotExecuted($seed);
if (!empty($missingDeps)) {
@@ -568,18 +594,12 @@ public function executeSeed(SeedInterface $seed, bool $force = false): void
' Auto-executing dependency: %s',
$depSeed->getName(),
));
- $this->executeSeed($depSeed, $force);
+ $this->executeSeed($depSeed, $force, $fake);
}
}
$this->printSeedStatus($seed, 'seeding');
- // Ensure seed schema table exists
- $adapter = $this->getEnvironment()->getAdapter();
- if (!$adapter->hasTable($adapter->getSeedSchemaTableName())) {
- $adapter->createSeedSchemaTable();
- }
-
// Execute the seeder and log the time elapsed.
$start = microtime(true);
$this->getEnvironment()->executeSeed($seed);
@@ -794,10 +814,11 @@ public function rollback(int|string|null $target = null, bool $force = false, bo
*
* @param string|null $seed Seeder
* @param bool $force Force re-execution even if seed has already been executed
+ * @param bool $fake Record seed as executed without actually running it
* @throws \InvalidArgumentException
* @return void
*/
- public function seed(?string $seed = null, bool $force = false): void
+ public function seed(?string $seed = null, bool $force = false, bool $fake = false): void
{
$seeds = $this->getSeeds();
@@ -805,14 +826,14 @@ public function seed(?string $seed = null, bool $force = false): void
// run all seeders
foreach ($seeds as $seeder) {
if (array_key_exists($seeder->getName(), $seeds)) {
- $this->executeSeed($seeder, $force);
+ $this->executeSeed($seeder, $force, $fake);
}
}
} else {
// run only one seeder
$normalizedName = $this->normalizeSeedName($seed, $seeds);
if ($normalizedName !== null) {
- $this->executeSeed($seeds[$normalizedName], $force);
+ $this->executeSeed($seeds[$normalizedName], $force, $fake);
} else {
throw new InvalidArgumentException(sprintf('The seed `%s` does not exist', $seed));
}
diff --git a/tests/TestCase/Command/SeedCommandTest.php b/tests/TestCase/Command/SeedCommandTest.php
index 77935ae4..ac240171 100644
--- a/tests/TestCase/Command/SeedCommandTest.php
+++ b/tests/TestCase/Command/SeedCommandTest.php
@@ -570,4 +570,144 @@ public function testNonIdempotentSeedIsTracked(): void
$this->assertOutputContains('already executed');
$this->assertOutputNotContains('seeding');
}
+
+ public function testFakeSeedMarksAsExecuted(): void
+ {
+ $this->createTables();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ // Run with --fake flag
+ $this->exec('seeds run -c test NumbersSeed --fake');
+ $this->assertExitSuccess();
+ $this->assertErrorContains('performing fake seeding');
+ $this->assertOutputContains('faking');
+ $this->assertOutputContains('faked');
+ $this->assertOutputNotContains('seeding');
+
+ // Verify NO data was inserted
+ $query = $connection->execute('SELECT COUNT(*) FROM numbers');
+ $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\'');
+ $this->assertEquals(1, $seedLog->fetchColumn(0), 'Fake seeds should be tracked');
+
+ // Running again should show already executed
+ $this->exec('seeds run -c test NumbersSeed');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('already executed');
+ }
+
+ public function testFakeSeedWithForce(): void
+ {
+ $this->createTables();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ // Run with --fake first
+ $this->exec('seeds run -c test NumbersSeed --fake');
+ $this->assertExitSuccess();
+
+ // Verify seed is tracked
+ $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
+ $this->exec('seeds run -c test NumbersSeed --force');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('seeding');
+
+ // Verify data was inserted
+ $query = $connection->execute('SELECT COUNT(*) FROM numbers');
+ $this->assertEquals(1, $query->fetchColumn(0));
+ }
+
+ public function testResetSpecificSeed(): void
+ {
+ $this->createTables();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ // Run two seeds
+ $this->exec('seeds run -c test NumbersSeed');
+ $this->assertExitSuccess();
+
+ $this->exec('seeds run -c test StoresSeed');
+ $this->assertExitSuccess();
+
+ // Verify both are tracked
+ $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\'');
+ $this->assertEquals(1, $storesLog->fetchColumn(0));
+
+ // Reset only Numbers seed
+ $this->exec('seeds reset -c test --seed Numbers', ['y']);
+ $this->assertExitSuccess();
+ $this->assertOutputContains('The following seeds will be reset:');
+ $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\'');
+ $this->assertEquals(0, $numbersLog->fetchColumn(0), 'Numbers seed should be reset');
+
+ $storesLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'StoresSeed\'');
+ $this->assertEquals(1, $storesLog->fetchColumn(0), 'Stores seed should still be tracked');
+ }
+
+ public function testResetMultipleSpecificSeeds(): void
+ {
+ $this->createTables();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ // Run seeds
+ $this->exec('seeds run -c test NumbersSeed');
+ $this->exec('seeds run -c test StoresSeed');
+
+ // Reset both with comma-separated list
+ $this->exec('seeds reset -c test --seed Numbers,Stores', ['y']);
+ $this->assertExitSuccess();
+
+ // Verify both are reset
+ $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\'');
+ $this->assertEquals(0, $storesLog->fetchColumn(0));
+ }
+
+ public function testResetNonExistentSeed(): void
+ {
+ $this->createTables();
+
+ $this->exec('seeds reset -c test --seed NonExistent');
+ $this->assertExitError();
+ $this->assertErrorContains('Seed `NonExistent` does not exist');
+ }
+
+ public function testFakeIdempotentSeedIsSkipped(): void
+ {
+ $this->createTables();
+
+ /** @var \Cake\Database\Connection $connection */
+ $connection = ConnectionManager::get('test');
+
+ // Run idempotent seed with --fake flag
+ $this->exec('seeds run -c test -s TestSeeds IdempotentTest --fake');
+ $this->assertExitSuccess();
+ $this->assertOutputContains('skipped (idempotent)');
+ $this->assertOutputNotContains('faking');
+ $this->assertOutputNotContains('faked');
+
+ // Verify the seed was NOT tracked (idempotent seeds are never tracked)
+ $seedLog = $connection->execute('SELECT COUNT(*) FROM cake_seeds WHERE seed_name = \'IdempotentTestSeed\'');
+ $this->assertEquals(0, $seedLog->fetchColumn(0), 'Idempotent seeds should not be tracked even when faked');
+ }
}