Skip to content

[5.x]: Craft 4→5 Matrix upgrade can silently orphan all content when project config was already migrated #19559

Description

@MisterMike

What happened?

Description

Upgrading a Craft 4 project to Craft 5 by importing a fresh Craft 4 database dump into an already-upgraded Craft 5 codebase (i.e. config/project/ is already committed in its post-upgrade Craft 5 shape from an earlier migration run) and running craft up can complete with no errors while silently destroying all Matrix content.

The entries rows, ownership rows, and elements_sites rows for the converted Matrix blocks are all created correctly, and the field values are correctly copied into elements_sites.content — but under JSON keys (field-layout-element UUIDs) that no longer match the live field layout by the time craft up finishes, so nothing resolves them. Meanwhile the legacy source tables (matrixblocks, matrixblocktypes, matrixcontent_*) are dropped unconditionally by m230617_070415_entrify_matrix_blocks.php regardless of whether the conversion actually produced anything, so there is no fallback and no warning.

Root cause, traced through vendor/craftcms/cms/src/...:

console/controllers/UpController::actionIndex() runs, in order:

  1. migrate/all --no-content — runs all pending Craft-track migrations, including:
    • migrations/m230511_215903_content_refactor.php (via BaseContentRefactorMigration::updateElements()), which copies each Matrix block's field values from the old per-column content table (e.g. matrixcontent_matrixcontent) into elements_sites.content, keyed by layoutElementUid (BaseContentRefactorMigration.php:154) — read from the block type's field layout as it exists at that moment, i.e. as freshly imported from the Craft 4 database.
    • migrations/m230617_070415_entrify_matrix_blocks.php, which converts the Matrix blocks into entries rows, reusing that same field layout object, then unconditionally drops matrixblocks, matrixblocks_owners, matrixblocktypes, and all matrixcontent_* tables at the end of safeUp() (lines 266–279) regardless of whether the earlier if (!empty($typeIdMap)) block (lines 200–264) actually populated anything.
  2. saveModifiedConfigData() + reset().
  3. project-config/apply — reconciles the DB's project config against the external config/project/ YAML. If that YAML was already committed in its Craft 5 shape from an earlier migration and has since been edited (in our case, a fields/merge that changed which field occupies a layout slot in a Matrix-derived entry type), it does not match what the fresh entrify run just produced. Applying it overwrites the entry types' field layouts — including the layout-element UUIDs that step 1 just wrote content under.
  4. migrate/up --track=content — runs project content migrations, which also key off the now-already-overwritten live layout.

Content ends up written under UUID A (matching the field layout at import time), then the field layout is replaced with one using UUID B for the same field, and nothing in the process re-keys the content or checks that the two still agree. The data loss is completely silent — no exception, no warning, no log entry.

Steps to reproduce

  1. Start with a Craft 5 project whose config/project/ was already fully migrated from Craft 4 in an earlier one-time run (Matrix fields already show entryTypes settings, no contentTable/matrixBlockTypes).
  2. Since that original migration, make a content-model change that regenerates layout-element UUIDs for one of the Matrix-derived entry types — e.g. run a fields/merge (craft\migrations\BaseFieldMergeMigration) to merge a duplicate field into the field it replaced. (Any edit to an affected entry type's field layout after the original migration appears to trigger this, not only field merges.)
  3. Import a fresh Craft 4 database dump (from the still-Craft-4 production site) into that Craft 5 codebase's database.
  4. Run craft up.
  5. Open any entry with the affected Matrix field in the control panel, or view the corresponding front-end page.

Expected behavior

Either the migration succeeds and all Matrix content is visible and correct, or it fails loudly / refuses to proceed if it detects that migrated content can't be reconciled with the field layout project-config/apply is about to impose.

Actual behavior

The migration reports success with no errors or warnings. The Matrix field shows zero content in the CP editor for every entry, and the front end renders empty Matrix sections. The data is not actually gone: elements_sites.content for the affected rows contains the correct, complete field values, but keyed by field-layout-element UUIDs that don't exist in the live field layout, so Craft correctly resolves "no value" for every field.

Claude and I were able to fully recover the content by restoring the pre-migration database backup (taken automatically by craft up), extracting the original matrixBlockTypes field-layout-element → field UUID mapping from its project config, cross-referencing it against the live (post-migration) field layouts by field UUID, and rewriting elements_sites.content keys accordingly. Happy to share the diagnostic queries/script if useful for a fix or test case.

Craft CMS version

5.10.14

PHP version

8.4.24

Operating system and version

No response

Database type and version

No response

Image driver and version

No response

Installed plugins and versions

  • not relevant / core migration issue

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions