What happened?
Description
fields/auto-merge fails with an integrity constraint violation when the two relation fields being merged share any (sourceId, sourceSiteId, targetId) combination.
craft\base\Field::afterMergeFrom() repoints the outgoing field's relations with an unguarded UPDATE:
// vendor/craftcms/cms/src/base/Field.php:933
Db::update(DbTable::RELATIONS, ['fieldId' => $this->id], ['fieldId' => $outgoingField->id]);
The relations table has a unique index on (fieldId, sourceId, sourceSiteId, targetId) (Install.php:943). If the same element relates the same target through both fields — e.g. an entry whose Hero Image and SEO Image point at the same asset — the UPDATE tries to create a duplicate and the migration aborts.
In my case, merging two Assets fields used across 12 entry types, 849 of 6,062 rows collided.
This isn't the same as #15869 (relation values being conflated), and the workaround recommended there does not prevent it. I accepted auto-merge's offer to run resave/all --with-fields=…, and it completed successfully immediately before the merge — roughly 21,000 elements_sites rows were rewritten. The merge then failed anyway. Resaving populates the content JSON, but it leaves the relations rows untouched, so the UPDATE still collides.
Partial-failure state
The failure leaves the install inconsistent, because the merge mutates the database before the content migration runs and $projectConfig->flush() is never reached:
|
State after failure |
| Outgoing field |
soft-deleted (dateDeleted set) |
| Its relation rows |
still on the old fieldId — never repointed |
| Field layouts (DB) |
already repointed to the persisting field |
| Project config (YAML + stored) |
still contains the outgoing field |
| Content migration |
not recorded as applied |
project-config/diff reports no pending changes, because the YAML and the stored project config agree — the divergence is between project config and the fields / fieldlayouts tables, which that command doesn't compare. So the inconsistency is invisible to the usual check.
Re-running the generated migration fails identically, since the colliding rows are still present.
Steps to reproduce
- Create two Assets fields,
fieldA and fieldB, with identical settings.
- Add both to the same entry type.
- On at least one entry, select the same asset in both fields, and save.
- Run
php craft fields/auto-merge and merge fieldA into fieldB. Accepting the
resave/all --with-fields=… prompt makes no difference.
Expected behavior
The merge either skips or de-duplicates the conflicting rows — the outgoing row is redundant once both layout elements resolve to the persisting field — and completes. Failing that, auto-merge detects the collision up front and refuses before mutating anything.
Actual behavior
→ Updating usages for heroImage … ✓ done
→ Removing heroImage … ✓ done
→ Generating content migration for heroImage … ✓ done
→ Running content migration for heroImage …
Exception: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
'217-314772-1-207012' for key 'relations.relations_fieldId_sourceId_sourceSiteId_targetId_unq_idx'
The SQL being executed was: UPDATE `relations` SET `fieldId`=217, `dateUpdated`='…' WHERE `fieldId`=1
#3 vendor/craftcms/cms/src/helpers/Db.php(1144): yii\db\Command->execute()
#4 vendor/craftcms/cms/src/base/Field.php(933): craft\helpers\Db::update()
#5 vendor/craftcms/cms/src/migrations/BaseFieldMergeMigration.php(75): craft\base\Field->afterMergeFrom()
#6 vendor/craftcms/cms/src/db/Migration.php(50): craft\migrations\BaseFieldMergeMigration->safeUp()
Craft CMS version
5.10.13.2 (Pro), schema 5.10.0.0
PHP version
8.3.30
Operating system and version
Linux (DDEV), MySQL 8.0.40
Database type and version
MySQL 8.0.40
Image driver and version
No response
Installed plugins and versions
Not relevant — reproducible with core Assets fields.
What happened?
Description
fields/auto-mergefails with an integrity constraint violation when the two relation fields being merged share any(sourceId, sourceSiteId, targetId)combination.craft\base\Field::afterMergeFrom()repoints the outgoing field's relations with an unguarded UPDATE:The
relationstable has a unique index on(fieldId, sourceId, sourceSiteId, targetId)(Install.php:943). If the same element relates the same target through both fields — e.g. an entry whose Hero Image and SEO Image point at the same asset — the UPDATE tries to create a duplicate and the migration aborts.In my case, merging two Assets fields used across 12 entry types, 849 of 6,062 rows collided.
This isn't the same as #15869 (relation values being conflated), and the workaround recommended there does not prevent it. I accepted
auto-merge's offer to runresave/all --with-fields=…, and it completed successfully immediately before the merge — roughly 21,000elements_sitesrows were rewritten. The merge then failed anyway. Resaving populates the content JSON, but it leaves therelationsrows untouched, so the UPDATE still collides.Partial-failure state
The failure leaves the install inconsistent, because the merge mutates the database before the content migration runs and
$projectConfig->flush()is never reached:dateDeletedset)fieldId— never repointedproject-config/diffreports no pending changes, because the YAML and the stored project config agree — the divergence is between project config and thefields/fieldlayoutstables, which that command doesn't compare. So the inconsistency is invisible to the usual check.Re-running the generated migration fails identically, since the colliding rows are still present.
Steps to reproduce
fieldAandfieldB, with identical settings.php craft fields/auto-mergeand mergefieldAintofieldB. Accepting theresave/all --with-fields=…prompt makes no difference.Expected behavior
The merge either skips or de-duplicates the conflicting rows — the outgoing row is redundant once both layout elements resolve to the persisting field — and completes. Failing that,
auto-mergedetects the collision up front and refuses before mutating anything.Actual behavior
Craft CMS version
5.10.13.2 (Pro), schema 5.10.0.0
PHP version
8.3.30
Operating system and version
Linux (DDEV), MySQL 8.0.40
Database type and version
MySQL 8.0.40
Image driver and version
No response
Installed plugins and versions
Not relevant — reproducible with core Assets fields.