Skip to content

Add unified cake_migrations table support with BC autodetect#965

Merged
markstory merged 32 commits into5.xfrom
5.x-unified-migrations-table
Dec 7, 2025
Merged

Add unified cake_migrations table support with BC autodetect#965
markstory merged 32 commits into5.xfrom
5.x-unified-migrations-table

Conversation

@dereuromark
Copy link
Copy Markdown
Member

@dereuromark dereuromark commented Nov 22, 2025

Summary

This PR introduces a consolidated migration tracking approach for v5.0, addressing #822.

Note: this is just one possible approach that combines BC with a clear forwards-approach.
@markstory I am still curious about your approach and where it could be a better fit.

New Features

  • cake_migrations table: Single table with plugin column to track all migrations (app + plugins)
  • UnifiedMigrationsTableStorage: New storage class for unified table operations
  • migrations upgrade command: Migrates data from legacy phinxlog tables to the new unified table

Backward Compatibility (Zero Breaking Changes)

  • Autodetect mode (default): If any phinxlog or *_phinxlog table exists, legacy mode is used automatically
  • Fresh installations: Automatically use the new cake_migrations table
  • No action required on upgrade - existing apps continue working with their phinxlog tables

Configuration

// config/app.php
'Migrations' => [
    'legacyTables' => null,  // (default) Autodetect - BC for existing, new table for fresh
    // 'legacyTables' => false, // Force unified cake_migrations table
    // 'legacyTables' => true,  // Force legacy phinxlog tables
]

Upgrade Workflow (for existing apps wanting to migrate)

  1. Upgrade to v5.0 (existing apps continue working with phinxlog - no changes needed)
  2. When ready, run bin/cake migrations upgrade --dry-run to preview
  3. Run bin/cake migrations upgrade to migrate data
  4. Set 'Migrations' => ['legacyTables' => false] in config
  5. Application now uses unified cake_migrations table

New Table Schema

CREATE TABLE cake_migrations (
    id INT AUTO_INCREMENT PRIMARY KEY,
    version BIGINT NOT NULL,
    migration_name VARCHAR(100) NULL,
    plugin VARCHAR(100) NULL,        -- NULL for app, 'PluginName' for plugins
    start_time TIMESTAMP NULL,
    end_time TIMESTAMP NULL,
    breakpoint TINYINT(1) NOT NULL DEFAULT 0,
    UNIQUE KEY version_plugin_unique (version, plugin)
);

Test Plan

  • PHPStan passes
  • PHPCS passes
  • Existing tests pass with legacyTables = true in bootstrap
  • Add tests for unified table mode
  • Add tests for upgrade command
  • Test with MySQL
  • Test with PostgreSQL
  • Test with SQLite

TODO

  • Add documentation for the upgrade process

This change introduces a consolidated migration tracking approach for v5.0:

**New Features:**
- `cake_migrations` table: Single table with `plugin` column to track all migrations
- `UnifiedMigrationsTableStorage`: New storage class for unified table operations
- `migrations upgrade` command: Migrates data from legacy phinxlog tables

**Backward Compatibility:**
- Autodetect mode (default): If any `phinxlog` or `*_phinxlog` table exists,
  legacy mode is used automatically - no breaking changes on upgrade
- Fresh installations automatically use the new `cake_migrations` table

**Configuration:**
- `Migrations.legacyTables = null` (default): Autodetect
- `Migrations.legacyTables = false`: Force unified table
- `Migrations.legacyTables = true`: Force legacy phinxlog tables

**Upgrade workflow:**
1. Upgrade to v5.0 (existing apps continue working with phinxlog)
2. Run `bin/cake migrations upgrade` to migrate data
3. Set `Migrations.legacyTables = false` in config
4. Application now uses unified `cake_migrations` table

Refs #822

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
*/
protected function isUsingUnifiedTable(): bool
{
$config = Configure::read('Migrations.legacyTables');
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.

This should be added to the documentation and the upgrade guide, as we'll need a guide so folks know how to upgrade and rollback between schema if necessary.

dereuromark and others added 6 commits November 23, 2025 11:06
- Simplify NULL handling using 'plugin IS' => $this->plugin pattern
- Remove cache complexity from UtilTrait
- Replace empty() with explicit === [] check
- Simplify upgradeTable() to no-op for new table format
- Use in_array for phinxlog detection (simpler than loop)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Uses the new hasTable() method from CakePHP 5.3 as suggested
in the PR feedback.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add LEGACY_TABLES CI matrix option to test both modes
- Use schemaDialect()->hasTable() for phinxlog detection
- Add documentation for unified table upgrade process
- Add basic test for UnifiedMigrationsTableStorage
- Update bootstrap to read LEGACY_TABLES from environment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When `Migrations.legacyTables` is set to `false`, the upgrade command
is not needed since the user has already opted into the unified table.
Only show it when in autodetect mode (null) or legacy mode (true).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@dereuromark
Copy link
Copy Markdown
Member Author

dereuromark commented Nov 23, 2025

I tested it on dereuromark/cakephp-sandbox#99
and it seems to work already really well.
The config flag also makes sure it is clean afterwards (no legacy stuff remaining).

A rerun with

bin/cake migrations upgrade --drop-tables

also cleans out the old empty ones.

Added LEGACY_TABLES=false matrix option for MySQL PHP 8.3

dereuromark and others added 4 commits November 24, 2025 00:41
The unified table feature works, but tests have extensive hardcoded
phinxlog assumptions (99 failures). Removing CI matrix for now.

Partial fixes to BakeMigrationDiffCommandTest to show the pattern:
- Add UtilTrait to get correct schema table name
- Update table cleanup to include both table types
- Update queries to use dynamic table name

TODO: Follow-up PR needed to update all tests for both table modes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add helper methods to TestCase for migration table operations:
  - getMigrationsTableName() - gets correct table name based on mode
  - clearMigrationRecords() - clears records with plugin filtering
  - getMigrationRecordCount() - counts records with plugin filtering
  - insertMigrationRecord() - inserts with correct structure
  - isUsingUnifiedTable() - checks current mode

- Update command tests to use new helper methods:
  - MigrateCommandTest
  - RollbackCommandTest
  - StatusCommandTest
  - MarkMigratedTest
  - DumpCommandTest
  - BakeMigrationDiffCommandTest

- Add cleanup for both phinxlog and cake_migrations tables
- Re-add LEGACY_TABLES=false CI matrix option

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
dereuromark added a commit to dereuromark/cakephp-test-helper that referenced this pull request Nov 24, 2025
Check Migrations.legacyTables config to use the correct table name.
When set to false, use the new 'cake_migrations' table instead of 'phinxlog'.

See: cakephp/migrations#965

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Update Util::tableName() to return cake_migrations when unified table mode is enabled
- Update Manager::cleanupMissingMigrations() to use correct table name and filter by plugin
- Skip cake_migrations table in bake snapshot/diff commands (like phinxlog tables)
- Update TableFinder to skip cake_migrations table
- Update Migrator to not drop cake_migrations during table cleanup
- Update tests to use helper methods for migration record operations
- Add mode-aware assertions in adapter tests
- Skip some Migrator tests in unified mode that test legacy-specific behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@markstory markstory left a comment

Choose a reason for hiding this comment

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

I got part way through.

dereuromark and others added 10 commits November 29, 2025 18:57
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
I thought the conditional logic in Manager, and unwrapping decorators
pointed to a missing abstraction method. Since we're in a major anyways,
I could extend the interface and have better layering as well.
@markstory
Copy link
Copy Markdown
Member

markstory commented Dec 7, 2025

@dereuromark Do you know what is up with the tests? They don't fail for me locally.

@dereuromark
Copy link
Copy Markdown
Member Author

dereuromark commented Dec 7, 2025

This one lock file is super annoying to me too
The directory MigrationsDiffDecimalChange exists but only has a .gitkeep file. The test copies the initial migration file from the comparisons folder to this directory, but then the cleanup in setUp() or tearDown() is deleting these files.

I will take another quick look

The checkSync() method compares the last migration file version against
the last migrated version. After the test deletes the migration record
from the phinxlog table, the migration file still exists, causing checkSync()
to fail because it sees an unmigrated file.

The fix deletes the migration file after running migrate and before baking
the diff, so checkSync() passes correctly.
@dereuromark dereuromark marked this pull request as ready for review December 7, 2025 07:47
@dereuromark
Copy link
Copy Markdown
Member Author

claude fixed it:

Summary

Root Cause: The runDiffBakingTest() method was failing because the checkSync() method in BakeMigrationDiffCommand checks if the last migration file version matches the last migrated version. The test was:

  1. Copying the diff migration file (e.g., 20160415220805_TheDiffDefaultMysql.php)
  2. Running migrate (which applies all migrations including the copied one)
  3. Deleting the migration record for version 20160415220805 from the database
  4. Trying to bake a new diff

After step 3, the migration FILE still existed with version 20160415220805, but the last migrated version was now 20160414193900 (the previous migration). The checkSync() method saw this discrepancy and failed with "Your migrations history is not in sync with your migrations files."

Fix: Added unlink($destination) after deleting the migration record from the database. This removes the migration file so that checkSync() sees the last file as version 20160414193900, which matches the last migrated version.

@markstory
Copy link
Copy Markdown
Member

Thanks for figuring out what was going on there. I wasn't able to reproduce locally.

After step 3, the migration FILE still existed with version 20160415220805, but the last migrated version was now 20160414193900 (the previous migration).

I don't see the 20160414193900 migration entry locally. This might be why I was getting different behavior.

@markstory markstory merged commit 79f7554 into 5.x Dec 7, 2025
14 checks passed
@markstory markstory deleted the 5.x-unified-migrations-table branch December 7, 2025 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants