Repository navigation
Fix #363: Add migrate:mark to move migration history to a version without executing migrations
#364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
f03ccd8
Add migrate:mark command to manage migration history
samdark 68a89e2
Apply PHP CS Fixer and Rector changes (CI)
github-actions[bot] 23e528f
Warn about risks of modifying migration history
samdark cbd80cf
Fix timestamp-only marking and make history updates atomic
samdark 4c1d009
Accept digit-leading migration suffixes in migrate:mark
samdark 7c2f406
Fix namespace timestamp matching and recorded target lookup
samdark e9a23cc
Keep history helpers private and centralize table checks
samdark d1fd8f2
Add migrate mark command examples
samdark File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # Modifying migration history | ||
|
|
||
| > [!WARNING] | ||
| > **Do not use `migrate:mark` unless you understand exactly how changing migration history will affect your database.** | ||
| > | ||
| > This command changes history records without changing or checking the database schema or data. Incorrectly marking | ||
| > migrations can leave history out of sync with the database, skip required changes, or cause later migration commands | ||
| > to fail or lose data. A later `migrate:up` will execute migrations whose records were removed, and `migrate:down` can | ||
| > revert migrations marked as applied even though this command never executed them. | ||
| > | ||
| > Before proceeding, verify that the database already matches the intended migration state and make a backup. | ||
|
|
||
| When adopting migrations for an existing database, or after making a migration's changes manually, use `migrate:mark` | ||
| to move migration history to a particular version without executing `up()` or `down()`: | ||
|
|
||
| ```shell | ||
| ./vendor/bin/yii-db-migration migrate:mark 'App\Migrations\M260101000002CreateIndex' | ||
| ./vendor/bin/yii-db-migration migrate:mark M260101000002CreateIndex | ||
| ./vendor/bin/yii-db-migration migrate:mark 260101000002 | ||
| ``` | ||
|
|
||
| Use the full class name, including its namespace when present, or a 12-digit migration timestamp. A timestamp with an | ||
| underscore between date and time (`260101_000002`) is also accepted. These are migration filename timestamps, not Unix | ||
| timestamps or date strings. If several migrations share a timestamp, the first matching migration in the relevant list | ||
| is selected; use its full class name to select an exact target. | ||
|
|
||
| The command modifies history as follows: | ||
|
|
||
| - If the target is pending, record all pending migrations in discovery order through and including the target. | ||
| Existing history entries remain unchanged. | ||
| - If the target is already recorded, remove entries newer than it in migration history, keeping the target and older | ||
| entries. This uses application order, not filename order, and does not require the recorded migration files to exist. | ||
| - If the target is already the latest recorded entry, nothing changes. | ||
| - If the target is unknown, report an error without adding or removing history entries. | ||
|
|
||
| To clear the entire history, use the special base version: | ||
|
|
||
| ```shell | ||
| ./vendor/bin/yii-db-migration migrate:mark m000000_000000_base | ||
| ``` | ||
|
|
||
| The command asks for confirmation before adding or removing entries. Use `--force-yes` (`-y`) for unattended execution. | ||
| With multiple databases configured, select one explicitly using `--db=maps`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Yiisoft\Db\Migration\Command; | ||
|
|
||
| use Symfony\Component\Console\Attribute\AsCommand; | ||
| use Symfony\Component\Console\Command\Command; | ||
| use Symfony\Component\Console\Formatter\OutputFormatter; | ||
| use Symfony\Component\Console\Input\InputArgument; | ||
| use Symfony\Component\Console\Input\InputInterface; | ||
| use Symfony\Component\Console\Input\InputOption; | ||
| use Symfony\Component\Console\Output\OutputInterface; | ||
| use Symfony\Component\Console\Style\SymfonyStyle; | ||
| use Yiisoft\Db\Migration\DatabaseContext; | ||
| use Yiisoft\Db\Migration\DatabaseSetRegistry; | ||
| use Yiisoft\Db\Migration\Migrator; | ||
| use Yiisoft\Db\Migration\Service\MigrationService; | ||
|
|
||
| use function array_keys; | ||
| use function array_map; | ||
| use function array_slice; | ||
| use function in_array; | ||
| use function preg_match; | ||
| use function str_replace; | ||
| use function strlen; | ||
| use function trim; | ||
|
|
||
| /** | ||
| * Moves migration history to a version without executing up() or down(). | ||
| * | ||
| * Use this command only after verifying that the database schema already matches the target migration state. | ||
| * | ||
| * For example, | ||
| * | ||
| * ```shell | ||
| * ./yii migrate:mark 'App\Migrations\M260101000002CreateIndex' # record pending migrations through this class | ||
| * ./yii migrate:mark M260101000002CreateIndex # use an unqualified migration class name | ||
| * ./yii migrate:mark 260101000002 # select a migration by its timestamp | ||
| * ./yii migrate:mark m000000_000000_base # remove all migration history entries | ||
| * ``` | ||
| * | ||
| * A pending target is recorded along with all earlier pending migrations. A recorded target removes newer history | ||
| * entries. Use the full class name when several migrations share a timestamp. | ||
| */ | ||
| #[AsCommand('migrate:mark', 'Modifies migration history without executing migrations. WARNING: Use only if you understand the consequences; incorrect history can cause data loss.')] | ||
| final class MarkCommand extends DatabaseCommand | ||
| { | ||
| public const BASE_MIGRATION = 'm000000_000000_base'; | ||
|
|
||
| public function __construct( | ||
| private readonly MigrationService $migrationService, | ||
| private readonly Migrator $migrator, | ||
| ?DatabaseSetRegistry $databases = null, | ||
| ) { | ||
| parent::__construct($databases); | ||
| } | ||
|
|
||
| protected function configure(): void | ||
| { | ||
| $this | ||
| ->addArgument('version', InputArgument::REQUIRED, 'Migration class name, timestamp, or m000000_000000_base to clear history.') | ||
| ->addOption('force-yes', 'y', InputOption::VALUE_NONE, 'Force yes to all questions.'); | ||
| } | ||
|
|
||
| protected function executeForDatabase( | ||
| InputInterface $input, | ||
| OutputInterface $output, | ||
| ?DatabaseContext $context, | ||
| ): int { | ||
| $migrator = $context->migrator ?? $this->migrator; | ||
| $service = $context->migrationService ?? $this->migrationService; | ||
| $io = new SymfonyStyle($input, $output); | ||
| $migrator->setIo($io); | ||
| $service->setIo($io); | ||
| $service->databaseConnection(); | ||
|
|
||
| /** @var string $version */ | ||
| $version = $input->getArgument('version'); | ||
| $version = trim($version, '\\'); | ||
| $label = OutputFormatter::escape($version); | ||
| $timestamp = preg_match('/^\d{6}_?\d{6}$/D', $version) === 1 | ||
| ? str_replace('_', '', $version) | ||
| : null; | ||
|
|
||
| if ($version !== self::BASE_MIGRATION && $timestamp === null | ||
| && preg_match('/^(?:\w+\\\\)*M\d{12}.*$/D', $version) !== 1 | ||
| ) { | ||
| $io->error('The version must be a migration class name, a migration timestamp, or ' . self::BASE_MIGRATION . '.'); | ||
| return Command::INVALID; | ||
| } | ||
|
|
||
| $add = []; | ||
| $remove = []; | ||
| $found = false; | ||
| $history = array_keys($migrator->getHistory()); | ||
| $isRecordedClass = $timestamp === null && in_array( | ||
| $version, | ||
| array_map(static fn(string $name): string => trim($name, '\\'), $history), | ||
| true, | ||
| ); | ||
|
|
||
| // Exact recorded classes do not need source files. Timestamps still select pending targets first. | ||
| if ($version !== self::BASE_MIGRATION && !$isRecordedClass) { | ||
| $pending = $service->getNewMigrations(); | ||
| foreach ($pending as $i => $migration) { | ||
| if ($this->matches($migration, $version, $timestamp)) { | ||
| $add = array_slice($pending, 0, $i + 1); | ||
| $found = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!$found) { | ||
| $history[] = self::BASE_MIGRATION; | ||
| foreach ($history as $i => $migration) { | ||
| if ($this->matches($migration, $version, $timestamp)) { | ||
| $remove = array_slice($history, 0, $i); | ||
| $found = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!$found) { | ||
| $io->error("Unable to find the version '$label'."); | ||
| return Command::INVALID; | ||
| } | ||
|
|
||
| if ($add === [] && $remove === []) { | ||
| $io->success("Already at '$label'. Nothing needs to be done."); | ||
| return Command::SUCCESS; | ||
| } | ||
|
|
||
| $limit = $migrator->getMigrationNameLimit(); | ||
| foreach ($add as $migration) { | ||
| if ($limit !== null && strlen($migration) > $limit) { | ||
| $io->error('The migration name "' . OutputFormatter::escape($migration) . '" is too long.'); | ||
| return Command::INVALID; | ||
| } | ||
| } | ||
|
|
||
| $io->note('Only migration history will change. No migrations will be applied or reverted.'); | ||
| if ($input->getOption('force-yes') || $io->confirm("Set migration history at $label?", false)) { | ||
| $migrator->updateHistory($add, $remove); | ||
| $io->success("The migration history is set at $label. No actual migration was performed."); | ||
| } | ||
|
|
||
| return Command::SUCCESS; | ||
| } | ||
|
|
||
| private function matches(string $migration, string $version, ?string $timestamp): bool | ||
| { | ||
| if ($timestamp === null) { | ||
| return trim($migration, '\\') === $version; | ||
| } | ||
|
|
||
| return preg_match('/(?:^|\\\\)M' . $timestamp . '[^\\\\]*$/D', $migration) === 1; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.