parser/diff: extend -- myschema:renamed-from to CHECK constraints and FKs#37
Conversation
… FKs MySQL has no in-place RENAME CONSTRAINT or RENAME FOREIGN KEY, so the diff still emits DROP+ADD for both. But threading the directive through gives the user a typo guard: a `-- myschema:renamed-from` whose source name doesn't exist on the current side aborts the plan with `renamed-from: source ... not found in current schema` instead of silently dropping + re-adding the wrong target. Implementation mirrors the existing column / index rename path: - `model.Constraint` and `model.ForeignKey` gain a `RenameFrom *string`. - `parser.classifyInlineLine` distinguishes the four named-constraint shapes by looking at the keyword after the name: UNIQUE → index (existing), CHECK → new inlineKindCheck, FOREIGN → new inlineKindFK. The catch-all `inlineKindConstraint` is removed (UNIQUE / CHECK / FOREIGN KEY cover every named CONSTRAINT shape MySQL accepts). - `parser.InlineRenames` gains `Constraints` and `ForeignKeys` maps; `ParseSQL` attaches them to the corresponding model entries the same way it does for columns and indexes. - `diff.validateConstraintRenames` / `validateForeignKeyRenames` run inside `diffTable` after the column / index rename passes. They enforce the same shape rules as the rename ALTER passes (duplicate source, source-also-declared, source-not-found, destination-already-exists, idempotent re-apply) but emit no statements — the natural diff path produces the DROP+ADD. Tests: - parser: TestExtractInlineRenamesCheckConstraint / TestExtractInlineRenamesForeignKey assert the new routing; TestParseSQLAcceptsConstraintRenameDirective / TestParseSQLAcceptsForeignKeyRenameDirective cover the parser→model attach. The previous "constraint is unsupported" expectations are flipped (or removed where they no longer apply). - diff: TestDiffRenameConstraintMissingSourceErrors / TestDiffRenameForeignKeyMissingSourceErrors lock in the typo-guard error path. - end-to-end YAML fixtures (`testdata/apply/`) for the happy path on both CHECK and FK with `verify_no_drift: true`, plus a `testdata/plan/` fixture for the typo error. Verified on mysql 8.0 + 9.4: full Go suite, YAML harness, scenario suite, lint all green. 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 #37 +/- ##
==========================================
+ Coverage 73.40% 73.67% +0.27%
==========================================
Files 27 27
Lines 2151 2234 +83
==========================================
+ Hits 1579 1646 +67
- Misses 412 425 +13
- Partials 160 163 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
edge paths PR #37's first cut only pinned the source-not-found path on each side. Add the duplicate-source and source-also-declared paths too — both mirror the existing column / index rename validation but lived without dedicated coverage for the constraint / FK side. - TestDiffRenameConstraintDuplicateSourceErrors / …ForeignKey… Two desired-side entries claim the same RenameFrom; without the guard the second would surface as a confusing "source not found" after the first apply consumed the source row. - TestDiffRenameConstraintSourceAlsoDeclaredErrors / …ForeignKey… Desired declares both the source name and the destination (renamed-from source); without the guard the diff would DROP+ADD the destination and then ADD the source, almost certainly not what the user meant. Parser-side `target … not found in CREATE TABLE` paths were considered but are unreachable in practice — `ExtractInlineRenames` and `parseCreateTable` resolve the same name through different code but normalised the same way, so the error never fires for any input that survives parsing. CLAUDE.md explicitly tells us not to write unnatural tests for unreachable defensive code, so they're intentionally omitted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Extends the existing -- myschema:renamed-from directive pipeline so it can also target named CHECK constraints and named foreign keys as a plan-time typo guard (while still emitting MySQL’s required DROP+ADD behavior for those objects).
Changes:
- Parser: routes inline rename directives on
CONSTRAINT ... CHECKandCONSTRAINT ... FOREIGN KEYlines into newInlineRenamesbuckets and attaches them tomodel.Constraint/model.ForeignKey. - Diff: adds validation passes that fail planning when the directive’s source name does not exist on the current side (typo guard), without emitting additional DDL.
- Tests/fixtures/docs: adds parser+diff tests and end-to-end YAML fixtures; updates AGENTS.md and removes the completed TODO.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| testdata/plan/rename_constraint_typo.yml | Plan fixture asserting typo-guard error for CHECK constraint rename source not found. |
| testdata/apply/rename_foreign_key_via_directive.yml | Apply fixture covering FK rename directive behavior (DROP FK + ADD CONSTRAINT) with no drift. |
| testdata/apply/rename_check_constraint_via_directive.yml | Apply fixture covering CHECK rename directive behavior (DROP CHECK + ADD CONSTRAINT) with no drift. |
| parser/parser.go | Attaches parsed inline constraint/FK rename directives onto model objects during ParseSQL. |
| parser/directive_test.go | Updates/adds tests validating directive extraction and parser attachment for CHECK constraints and FKs. |
| parser/directive.go | Extends inline directive classification and InlineRenames to support CHECK constraints and FKs. |
| model/foreignkey.go | Adds RenameFrom *string to ForeignKey for plan-time validation. |
| model/constraint.go | Adds RenameFrom *string to Constraint (CHECK-only in practice) for plan-time validation. |
| diff/tables.go | Runs new constraint/FK rename validation inside diffTable before normal diffs. |
| diff/rename_test.go | Adds diff-layer tests asserting missing-source rename directives fail loudly. |
| diff/rename.go | Implements validation helpers + duplicate-source detection for constraint/FK rename directives. |
| TODO.md | Removes completed “extend renamed-from to constraints and FKs” high-priority item. |
| AGENTS.md | Updates repo documentation to reflect new directive support and semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two valid nits:
- diff/rename.go: validateConstraintRenames docstring said "the
natural diff path produces the DROP CONSTRAINT + ADD CONSTRAINT".
For CHECK constraints diffConstraints actually emits
`DROP CHECK <name>` (not MySQL's `DROP CONSTRAINT` syntax) plus
`ADD CONSTRAINT <name> CHECK (...)`. Update the wording so it
matches the emitted DDL.
- parser/parser.go: rejectMisplacedRenameDirectives only inspected
inline.Columns / inline.Indexes / inline.Unsupported, but
ExtractInlineRenames now also fills inline.Constraints and
inline.ForeignKeys for the new CHECK / FK directive paths. Without
that, an inline CHECK or FK rename directive sitting in a
non-CREATE-TABLE statement could in principle slip through
silently. Extend the guard to cover both new maps so the
consistency invariant ("any non-empty inline-renames map on a
non-CREATE-TABLE statement is an error") holds across all five
buckets.
No new tests for the parser-side change: the only inline shapes
classifyInlineLine routes to Constraints / ForeignKeys are
`CONSTRAINT name CHECK` / `CONSTRAINT name FOREIGN KEY` lines,
which only appear inside CREATE TABLE bodies in any SQL the project
parses. The reachable regression already lives in the existing
TestParseSQLRejectsRenameDirectiveOnCreateView; expanding it with
artificially-constructed inline cases would be an unnatural test of
unreachable defensive code (per CLAUDE.md).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Both classifyInlineLine CONSTRAINT branches said "Distinguish the four named-constraint shapes" but the switch only handles three (UNIQUE / CHECK / FOREIGN). The fourth shape that fits the comment is `CONSTRAINT name PRIMARY KEY (...)` — legal MySQL syntax, but the user-supplied name is ignored in favour of the fixed "PRIMARY", so the directive on it can't drive a real rename and the code intentionally falls through to "unknown". Reword both comments as "rename-eligible named-constraint shapes" and explicitly call out `CONSTRAINT name PRIMARY KEY` as the unhandled fourth so a reader doesn't think a switch case is missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 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
Closes the High — production-gates TODO entry. The
-- myschema:renamed-fromdirective now accepts CHECK constraints and foreign keys as targets in addition to tables, columns, and secondary indexes.MySQL has no in-place
RENAME CONSTRAINTorRENAME FOREIGN KEY, so the diff still emits DROP+ADD for both. The directive's value is purely as a typo guard at plan time: a-- myschema:renamed-fromwhose source name doesn't exist on the current side aborts the plan withrenamed-from: source ... not found in current schemainstead of silently dropping + re-adding the wrong target.Example
Implementation
Mirrors the existing column / index rename path:
model.Constraint/model.ForeignKeygainRenameFrom *string.parser.classifyInlineLinedistinguishes the four named-constraint shapes by looking at the keyword after the name: UNIQUE → index (existing), CHECK → newinlineKindCheck, FOREIGN → newinlineKindFK. The catch-allinlineKindConstraintis removed (UNIQUE / CHECK / FOREIGN KEY cover every named CONSTRAINT shape MySQL accepts).parser.InlineRenamesgainsConstraintsandForeignKeysmaps;ParseSQLattaches them to the corresponding model entries.diff.validateConstraintRenames/validateForeignKeyRenamesrun insidediffTableafter the column / index rename passes. Same shape rules as the existing rename ALTER passes (duplicate source, source-also-declared, source-not-found, destination-already-exists, idempotent re-apply) but emit no statements — the natural diff path produces the DROP+ADD.Tests
TestExtractInlineRenamesCheckConstraint/TestExtractInlineRenamesForeignKeyfor the new routing;TestParseSQLAcceptsConstraintRenameDirective/TestParseSQLAcceptsForeignKeyRenameDirectivefor the parser→model attach. The old "constraint is unsupported" expectations are flipped or removed.TestDiffRenameConstraintMissingSourceErrors/TestDiffRenameForeignKeyMissingSourceErrorslock in the typo-guard error path.testdata/apply/rename_check_constraint_via_directive.yml(happy path,verify_no_drift: true)testdata/apply/rename_foreign_key_via_directive.yml(happy path,verify_no_drift: true)testdata/plan/rename_constraint_typo.yml(typo → plan error)Test plan
make test(8.0)make test-mysql9(9.4)make test-scenario(8.0)make lint🤖 Generated with Claude Code