Skip to content

Fixed migration failure when dropping a legacy index that backs a foreign key on MySQL - #1136

Merged
fballiano merged 1 commit into
mainfrom
fix-mysql-index-drop-backing-foreign-key
Jul 24, 2026
Merged

Fixed migration failure when dropping a legacy index that backs a foreign key on MySQL#1136
fballiano merged 1 commit into
mainfrom
fix-mysql-index-drop-backing-foreign-key

Conversation

@fballiano

Copy link
Copy Markdown
Contributor

./maho migrate aborts on a legacy install:

8 destructive statement(s):
  ...
  drop index UNQ_REPORT_COMPARED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID on report_compared_product_index
  ...
An exception occurred while executing a query: SQLSTATE[HY000]: General error: 1553
Cannot drop index 'UNQ_REPORT_COMPARED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID':
needed in a foreign key constraint

Cause

That UNIQUE key is the only index covering FK ... (customer_id), and InnoDB refuses to drop it. Disabling FOREIGN_KEY_CHECKS does not lift the restriction, so startSetup() is no help; what does lift it is another index taking over the cover in the same statement.

DBAL knows this: AbstractMySQLPlatform::getPreAlterTableIndexForeignKeySQL() combines an index drop and an index creation over the same columns into one ALTER TABLE ... DROP INDEX a, ADD INDEX b (...). It just never fires for us, because it compares the two column lists through the legacy Index::getColumns() accessor, which returns the raw names: introspection marks them as quoted ("customer_id"), the declarative target keeps them bare (customer_id). Same accessor, same mismatch as #7392 on SQLite. Unfolded, DBAL emits every index drop before every index creation, so the drop of the only covering index runs first:

DROP INDEX `UNQ_REPORT_COMPARED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID` ON report_compared_product_index;
DROP INDEX `UNQ_REPORT_COMPARED_PRODUCT_INDEX_VISITOR_ID_PRODUCT_ID` ON report_compared_product_index;
CREATE INDEX IDX_97974EB670BEE6D4584665A ON report_compared_product_index (visitor_id, product_id);
CREATE INDEX IDX_97974EB69395C3F34584665A ON report_compared_product_index (customer_id, product_id);
CREATE INDEX IDX_97974EB69395C3F3 ON report_compared_product_index (customer_id);

Fix

Canonicalizer::unquoteIndexColumns() re-expresses every live index with bare column names, keeping its name (that quoting is load-bearing on Postgres, where a legacy hex-hash index name can start with a digit and is only valid quoted), its type and its prefix lengths. It sits with the introspection artifacts the class already reconciles, next to stripPhantomIndexes() and alignIndexNames().

The platform then combines the pairs as intended, and the same migration plans 3 statements:

ALTER TABLE report_compared_product_index DROP INDEX UNQ_..._CUSTOMER_ID_PRODUCT_ID, ADD INDEX IDX_...4584665A (customer_id, product_id);
ALTER TABLE report_compared_product_index DROP INDEX UNQ_..._VISITOR_ID_PRODUCT_ID, ADD INDEX IDX_...4584665A (visitor_id, product_id);
CREATE INDEX IDX_...9395C3F3 ON report_compared_product_index (customer_id);

Verification

Against a legacy-shaped fixture on MySQL 9.7 (report_compared_product_index and customer_entity as a Magento install leaves them):

  • before: 1553 on the first drop
  • after: every statement applies, and a second plan() returns 0 statements, so the schema converges
  • one test added to CanonicalizerTest, 26 passing, composer lint green

Upstream

The DBAL side is doctrine/dbal#7475, which fixes the comparison at the source. The workaround carries a @todo to drop it once a release carrying that fix is required, since the constraint is ^4.4 today.

One shape stays unfixed on DBAL 4.4 either way: an index covering a foreign key that the target drops without a same-column replacement has nothing to combine with, and still fails with 1553. DBAL 5.0 handles it structurally by putting all drops and adds into a single ALTER TABLE. No table in the current schemas is in that shape.

DBAL 4.4 introspection marks an index's column names as quoted, so the
same index reads as ["customer_id"] live and [customer_id] in the
declarative target. AbstractMySQLPlatform combines an index drop and an
index creation over the same columns into a single ALTER TABLE statement,
but it compares the two column lists through the legacy
Index::getColumns() accessor, which returns those raw names. The lists
never match for a live-vs-declarative diff, so the statements are not
combined and the drop is emitted on its own.

That aborts the migration whenever the dropped index is the one covering
a foreign key, which is every legacy install whose composite UNIQUE key
the declarative target re-declares as a plain index:

    ./maho migrate
    SQLSTATE[HY000]: General error: 1553 Cannot drop index
    'UNQ_REPORT_COMPARED_PRODUCT_INDEX_CUSTOMER_ID_PRODUCT_ID':
    needed in a foreign key constraint

InnoDB refuses the drop because no other index would cover the key, and
disabling FOREIGN_KEY_CHECKS does not lift the restriction, so
startSetup() is no help.

Re-express every live index with bare column names in the Canonicalizer,
alongside the introspection artifacts it already reconciles. The platform
then combines the pair as intended, the cover never lapses, and the
migration converges: the affected table now plans 3 statements instead of
5 and re-plans 0 on a second run.

The upstream fix is doctrine/dbal#7475; the workaround carries a @todo to
drop it once a release requiring it is in place.
@fballiano fballiano added this to the 26.9.0 milestone Jul 24, 2026
@fballiano fballiano added the backport 26.7 Backport this PR to the 26.7 maintenance branch label Jul 24, 2026
@fballiano
fballiano merged commit 2a9e2b9 into main Jul 24, 2026
28 checks passed
@github-actions

Copy link
Copy Markdown

@fballiano
fballiano deleted the fix-mysql-index-drop-backing-foreign-key branch July 24, 2026 21:41
fballiano added a commit that referenced this pull request Jul 24, 2026
…n key (#1136) (#1137)

(cherry picked from commit 2a9e2b9)

Co-authored-by: Fabrizio Balliano <fabrizio.balliano@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport 26.7 Backport this PR to the 26.7 maintenance branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant