diff: suppress DROP INDEX when its columns are also being dropped#23
Conversation
MySQL automatically removes any index whose every column is dropped in the same ALTER TABLE flow. Issuing an explicit `DROP INDEX i` afterwards then errors with `Error 1091 (42000): Can't DROP 'i'; check that column/key exists`. The bug was first surfaced while writing the wildcard YAML fixture in PR #21 — that fixture sidestepped the case by exercising table+constraint+view drops together. With this fix, --allow-drop=all on a column-with-its-only-index now applies cleanly. diff/tables.go: - diffTable computes a `droppedColumns` set (current columns absent from desired) once and passes it to diffIndexes. - diffIndexes adds an early-skip: pure DROP INDEX whose every part references a dropped column is silently dropped from the plan. Indexes with expression parts (no Column field) don't qualify — we don't track expression-column dependencies here. Tests: - TestDiffDropColumnSuppressesDependentIndex (single-col index) - TestDiffDropAllColumnsSuppressesCombinedIndex (multi-col, all dropped) - TestDiffDropPartialColumnKeepsIndex (multi-col, only some dropped — index DROP must still fire so the column drop succeeds) - TestDropPolicyWildcard rewritten to a clean "wildcard allows everything" assertion using a separate table; the column-dependent case is its own test now. YAML fixtures (3 added): - apply/drop_column_with_dependent_index — single-column case - apply/drop_two_columns_with_combined_index — multi-column suppression - apply/drop_one_of_two_index_columns — partial coverage; verifies the diff still emits DROP INDEX + new CREATE INDEX Verified manually: apply --allow-drop=all on `users(id, legacy, KEY i (legacy))` → desired `users(id)` now succeeds (previously errored 1091). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #23 +/- ##
==========================================
+ Coverage 69.79% 70.37% +0.58%
==========================================
Files 25 25
Lines 1652 1671 +19
==========================================
+ Hits 1153 1176 +23
+ Misses 381 375 -6
- Partials 118 120 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR updates the diff/apply planning logic to avoid emitting redundant DROP INDEX statements when the index will be implicitly removed by MySQL as part of dropping its underlying columns, preventing MySQL Error 1091 failures during apply.
Changes:
- Track columns being dropped in
diffTableand pass that set intodiffIndexes. - Suppress pure
DROP INDEXoperations when every index part references a column that is also being dropped. - Add Go unit tests and new YAML apply fixtures to validate suppression behavior across single-column, multi-column, and partial-drop scenarios.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
diff/tables.go |
Computes dropped-column set and uses it to suppress redundant DROP INDEX plans. |
diff/tables_test.go |
Adds targeted tests for dependent-index suppression and adjusts wildcard drop policy coverage. |
testdata/apply/drop_column_with_dependent_index.yml |
Apply fixture covering single-column dependent-index suppression. |
testdata/apply/drop_two_columns_with_combined_index.yml |
Apply fixture covering multi-column index suppression when all parts are dropped. |
testdata/apply/drop_one_of_two_index_columns.yml |
Apply fixture for partial-drop behavior (drop one column, keep/recreate index). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Three fixes — one bug, two doc / fixture corrections.
🔴 Bug: the new dependent-index suppression ran regardless of drop
policy. If a user passed --allow-drop=index but NOT
--allow-drop=column, the column drop would be suppressed (sent to
DisallowedDropStmts) yet my code still treated the column as
"going away" and silently dropped the explicit DROP INDEX. Net
effect: an orphaned index that nobody was managing.
Fix: gate the dropped-columns set on dc.IsDropAllowed("column"). If
the user hasn't allowed column drops, dropped is nil and
allPartsDropped never matches, so DROP INDEX is emitted as before.
New regression test:
TestDiffDropIndexNotSuppressedWhenColumnDropDisallowed.
🟢 Docs:
- diff/tables_test.go comment said `diff.allPartsDropped` —
the helper is in this package so the qualifier was misleading.
- testdata/apply/drop_one_of_two_index_columns.yml had a stale
comment claiming "this case would still fail at apply time"
with `verify_no_drift: false`, but the apply actually succeeds
(MySQL folds the multi-col index down to its surviving column
on DROP COLUMN; we then DROP INDEX the leftover and CREATE
the desired narrower form). Reworded the comment to match
reality and dropped `verify_no_drift: false`; the harness now
re-plans after apply and confirms no drift.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot review caught a remaining 1091 case my earlier fix didn't cover: the DROP+CREATE replacement path. When an index keeps the same name but moves to different columns AND the original columns are being dropped (e.g. KEY i (a) → KEY i (b) with column a gone), the diff emits DROP INDEX i + CREATE INDEX i (b). MySQL auto-removes the old index during DROP COLUMN a, so the explicit DROP INDEX still errors 1091. Drop the `!ok &&` qualifier from the suppression — the check now applies whether the index is being purely removed or replaced. The desired-side loop still emits the new CREATE INDEX, so the replacement converges. Tests: - TestDiffReplaceIndexSuppressesDependentDrop (Go) covers KEY i (a) → KEY i (b) with column a dropped. - testdata/apply/replace_index_with_column_drop.yml runs the same scenario end-to-end, and the harness's drift check verifies apply converges to the desired schema. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Make droppedColumns the single source of truth for the dropped-column set. It now returns []string (current's iteration order); diffColumns iterates the slice instead of duplicating the loop, and diffTable materialises the same slice into a map[string]bool for allPartsDropped lookups in diffIndexes. Behaviour unchanged. Addresses Copilot PR #23 review #3 inline comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Fix the
Error 1091apply failure surfaced while writing the wildcard YAML fixture in PR #21. MySQL automatically removes any index whose every column is dropped in the sameALTER TABLEflow — issuing an explicitDROP INDEX iafterwards then errors with:After this PR,
--allow-drop=allon a column-with-its-only-index applies cleanly. The fix also covers the DROP+CREATE replacement path (same index name, narrower column list) and is correctly gated by--allow-drop=columnso we never strand an index on a column that isn't actually being removed.Implementation
diff/tables.go:droppedColumns(current, desired) []string— single source of truth for the dropped-column set, in current's iteration order. Reused insidediffColumnsfor the drop loop and materialised into amap[string]boolindiffTablefor O(1) lookups inallPartsDropped.diffTableonly computes the dropped-column set when--allow-drop=columnis allowed; otherwise the column won't actually go away and we must NOT suppress the index drop (the index would be left orphaned-yet-unmanaged on a still-present column).diffIndexesearly-skips aDROP INDEXwhose every part references a dropped column. Applies to both pure removals and the DROP+CREATE replacement path — the desired-side loop still emits the freshCREATE INDEXfor the latter. Indexes with expression parts don't qualify (we don't track expression-column dependencies here).Tests
Go (
diff/tables_test.go):TestDiffDropColumnSuppressesDependentIndex— single-col index, suppressedTestDiffDropAllColumnsSuppressesCombinedIndex— multi-col, all parts dropped → suppressedTestDiffReplaceIndexSuppressesDependentDrop— replace path: same name, narrower cols → DROP suppressed, CREATE still emittedTestDiffDropIndexNotSuppressedWhenColumnDropDisallowed— drop policy gating:--allow-drop=columnunset → DROP INDEX still emittedTestDiffDropPartialColumnKeepsIndex— multi-col, only some parts dropped →DROP INDEXstill emittedTestDropPolicyWildcardrewritten to a clean wildcard-allows-everything assertion (the dependent-index case is its own test now)YAML fixtures (4):
apply/drop_column_with_dependent_index.yml— single-columnapply/drop_two_columns_with_combined_index.yml— multi-column suppressionapply/drop_one_of_two_index_columns.yml— partial coverage; verifies the diff still emitsDROP INDEX + new CREATE INDEXapply/replace_index_with_column_drop.yml— replace path covered end-to-endTest plan
make test(parser + diff + catalog + model + YAML harness)make test-scenariogolangci-lintusers(id, legacy, KEY i (legacy))→users(id)apply succeeds🤖 Generated with Claude Code