Skip to content

diff: suppress DROP INDEX when its columns are also being dropped#23

Merged
winebarrel merged 4 commits into
mainfrom
fix-correctness-bugs
May 2, 2026
Merged

diff: suppress DROP INDEX when its columns are also being dropped#23
winebarrel merged 4 commits into
mainfrom
fix-correctness-bugs

Conversation

@winebarrel

@winebarrel winebarrel commented May 2, 2026

Copy link
Copy Markdown
Owner

Summary

Fix the Error 1091 apply failure surfaced while writing the wildcard YAML fixture in PR #21. 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

After this PR, --allow-drop=all on 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=column so 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 inside diffColumns for the drop loop and materialised into a map[string]bool in diffTable for O(1) lookups in allPartsDropped.
  • diffTable only computes the dropped-column set when --allow-drop=column is 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).
  • diffIndexes early-skips a DROP INDEX whose every part references a dropped column. Applies to both pure removals and the DROP+CREATE replacement path — the desired-side loop still emits the fresh CREATE INDEX for 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, suppressed
  • TestDiffDropAllColumnsSuppressesCombinedIndex — multi-col, all parts dropped → suppressed
  • TestDiffReplaceIndexSuppressesDependentDrop — replace path: same name, narrower cols → DROP suppressed, CREATE still emitted
  • TestDiffDropIndexNotSuppressedWhenColumnDropDisallowed — drop policy gating: --allow-drop=column unset → DROP INDEX still emitted
  • TestDiffDropPartialColumnKeepsIndex — multi-col, only some parts dropped → DROP INDEX still emitted
  • TestDropPolicyWildcard rewritten 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-column
  • apply/drop_two_columns_with_combined_index.yml — multi-column suppression
  • apply/drop_one_of_two_index_columns.yml — partial coverage; verifies the diff still emits DROP INDEX + new CREATE INDEX
  • apply/replace_index_with_column_drop.yml — replace path covered end-to-end

Test plan

  • make test (parser + diff + catalog + model + YAML harness)
  • make test-scenario
  • golangci-lint
  • All three sample DBs round-trip without drift
  • Manual repro: users(id, legacy, KEY i (legacy))users(id) apply succeeds
  • CI green on this PR

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 2, 2026 08:31
@codecov

codecov Bot commented May 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.37%. Comparing base (2a89694) to head (34205c7).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
diff/tables.go 91.66% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 diffTable and pass that set into diffIndexes.
  • Suppress pure DROP INDEX operations 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.

Comment thread diff/tables.go Outdated
Comment thread testdata/apply/drop_one_of_two_index_columns.yml Outdated
Comment thread testdata/apply/drop_one_of_two_index_columns.yml Outdated
Comment thread diff/tables_test.go Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread diff/tables.go Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread testdata/apply/replace_index_with_column_drop.yml
Comment thread diff/tables.go Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@winebarrel winebarrel merged commit f7c47c1 into main May 2, 2026
8 checks passed
@winebarrel winebarrel deleted the fix-correctness-bugs branch May 2, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants