Skip to content

parser/diff: extend -- myschema:renamed-from to CHECK constraints and FKs#37

Merged
winebarrel merged 4 commits into
mainfrom
extend-renamed-from-to-constraints-and-fks
May 3, 2026
Merged

parser/diff: extend -- myschema:renamed-from to CHECK constraints and FKs#37
winebarrel merged 4 commits into
mainfrom
extend-renamed-from-to-constraints-and-fks

Conversation

@winebarrel

Copy link
Copy Markdown
Owner

Summary

Closes the High — production-gates TODO entry. The -- myschema:renamed-from directive now accepts CHECK constraints and foreign keys as targets in addition to tables, columns, and secondary indexes.

MySQL has no in-place RENAME CONSTRAINT or RENAME 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-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.

Example

CREATE TABLE users (
    id BIGINT NOT NULL,
    age INT,
    org_id BIGINT NOT NULL,
    PRIMARY KEY (id),
    KEY idx_org (org_id),
    -- myschema:renamed-from chk_age_old
    CONSTRAINT chk_age_nonneg CHECK (age >= 0),
    -- myschema:renamed-from fk_users_org_old
    CONSTRAINT fk_users_org   FOREIGN KEY (org_id) REFERENCES orgs(id)
);
  • Source names exist on current → directives are no-ops; plan emits the natural DROP+ADD pair.
  • Either source missing → plan errors out before any DDL is sent to MySQL.

Implementation

Mirrors the existing column / index rename path:

  • model.Constraint / model.ForeignKey gain 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.
  • diff.validateConstraintRenames / validateForeignKeyRenames run inside diffTable after 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

  • parser: TestExtractInlineRenamesCheckConstraint / TestExtractInlineRenamesForeignKey for the new routing; TestParseSQLAcceptsConstraintRenameDirective / TestParseSQLAcceptsForeignKeyRenameDirective for the parser→model attach. The old "constraint is unsupported" expectations are flipped or removed.
  • diff: TestDiffRenameConstraintMissingSourceErrors / TestDiffRenameForeignKeyMissingSourceErrors lock in the typo-guard error path.
  • end-to-end YAML fixtures:
    • 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

… 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>
Copilot AI review requested due to automatic review settings May 3, 2026 02:46
@codecov

codecov Bot commented May 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.28866% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.67%. Comparing base (ce044af) to head (f968d0c).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
diff/rename.go 77.77% 8 Missing and 4 partials ⚠️
parser/directive.go 76.92% 5 Missing and 1 partial ⚠️
parser/parser.go 61.53% 3 Missing and 2 partials ⚠️
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.
📢 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.

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>

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

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 ... CHECK and CONSTRAINT ... FOREIGN KEY lines into new InlineRenames buckets and attaches them to model.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.

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

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 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.

Comment thread parser/directive.go Outdated
Comment thread parser/directive.go Outdated
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>

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 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.

@winebarrel winebarrel merged commit ce74e89 into main May 3, 2026
9 checks passed
@winebarrel winebarrel deleted the extend-renamed-from-to-constraints-and-fks branch May 3, 2026 03:15
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