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:
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.
saveModifiedConfigData() + reset().
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.
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
- 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).
- 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.)
- Import a fresh Craft 4 database dump (from the still-Craft-4 production site) into that Craft 5 codebase's database.
- Run
craft up.
- 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
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 runningcraft upcan complete with no errors while silently destroying all Matrix content.The
entriesrows, ownership rows, andelements_sitesrows for the converted Matrix blocks are all created correctly, and the field values are correctly copied intoelements_sites.content— but under JSON keys (field-layout-element UUIDs) that no longer match the live field layout by the timecraft upfinishes, so nothing resolves them. Meanwhile the legacy source tables (matrixblocks,matrixblocktypes,matrixcontent_*) are dropped unconditionally bym230617_070415_entrify_matrix_blocks.phpregardless 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:migrate/all --no-content— runs all pending Craft-track migrations, including:migrations/m230511_215903_content_refactor.php(viaBaseContentRefactorMigration::updateElements()), which copies each Matrix block's field values from the old per-column content table (e.g.matrixcontent_matrixcontent) intoelements_sites.content, keyed bylayoutElementUid(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 intoentriesrows, reusing that same field layout object, then unconditionally dropsmatrixblocks,matrixblocks_owners,matrixblocktypes, and allmatrixcontent_*tables at the end ofsafeUp()(lines 266–279) regardless of whether the earlierif (!empty($typeIdMap))block (lines 200–264) actually populated anything.saveModifiedConfigData()+reset().project-config/apply— reconciles the DB's project config against the externalconfig/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, afields/mergethat 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.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
config/project/was already fully migrated from Craft 4 in an earlier one-time run (Matrix fields already showentryTypessettings, nocontentTable/matrixBlockTypes).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.)craft up.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/applyis 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.contentfor 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 originalmatrixBlockTypesfield-layout-element → field UUID mapping from its project config, cross-referencing it against the live (post-migration) field layouts by field UUID, and rewritingelements_sites.contentkeys 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