Skip to content

fix: a rename in place whose name is held when it runs is a rebuild - #138

Merged
CaffeinatedCoder merged 1 commit into
mainfrom
fix/rename-frees-name-late
Nov 14, 2026
Merged

CaffeinatedCoder merged 1 commit into
mainfrom
fix/rename-frees-name-late

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #99

This takes the place of #137, which GitHub closed when the branch of #133 it was stacked on was deleted at that merge. The commit is the same, rebased onto main, which now holds #133 and its ISchemaDialect.DefaultSchema that the comparison here needs.

Problem

A contributor's in-place statements are written after EF Core's operations. When an exclusion constraint is renamed and the same migration has EF Core create an index under the name the constraint gives up, CREATE INDEX runs while the constraint's index still holds that name. The migration scaffolds and fails at apply with 42P07. NameClaims.Require reads the end state only, where every name is taken once.

I measured this on PostgreSQL 18 against main @ 010ff31, before the fix. Four cases fail with 42P07:

  • An index takes a constraint's explicit old name.
  • An index takes a constraint's default old name, which changes because a column is renamed.
  • EF Core renames an index into the name a renamed constraint gives up.
  • Two constraints on two tables of one schema swap names. This case is not in the issue. It is the same defect among declarations: each in-place rename takes a relation name the other still holds.

Change

  • SchemaOperationPlanner.KeepInPlace.
    • Each InPlace answer is written into a SchemaSql of its own. Its statements reach the migration only if the change stays in place.
    • A change whose claims differ (a rename) is compared, in the order the changes in place run, with what holds a name at that moment:
      • EF Core's objects as the target has them, since all of EF Core's operations have run.
      • The declared objects of the source as EF Core's operations leave them (AfterOperations: tables and columns renamed, tables moved), except those dropped before EF Core's operations or lost with them. A BeforeTables object is dropped last, so it still holds its name.
      • The earlier changes in place, under their new names.
    • Both the name given up and the name taken are compared. A change that collides becomes a rebuild: the drop runs before EF Core's operations, the create runs after them and after every change in place, and the create ensures its extension like any other. No temporary names.
    • A BeforeTables contributor's rename that collides is refused (SchemaKitStrings.RenameBeforeTablesCollides), because such an object cannot be rebuilt around tables.
  • NameClaims. Taken, Of, Collide and OfTheModel are the helpers that Require and the planner now share. There is one comparison, with one set of namespace rules.
  • SchemaSql.Append (internal).
  • CLAUDE.md: one bullet under "The planner".

The mirror case, on the server

A constraint renamed into a name that an EF Core index gives up in the same migration (dropped or renamed away) applies today, and keeps its oid. EF Core's operation runs before the rename. The planner therefore does not treat a name EF Core gives up as a collision, and the rename stays one RENAME CONSTRAINT. Tests:

  • A_renamed_constraint_takes_the_name_of_an_index_that_is_dropped (live)
  • A_renamed_constraint_takes_the_name_of_an_index_that_is_renamed (live)
  • A_rename_into_a_name_an_index_gives_up_stays_a_rename (statements)

Unchanged

  • Swaps and chains on one table were already rebuilt, because a pair is matched by name first. They still are, statement for statement. These tests pass with the rule removed: Constraints_on_one_table_that_swap_names_or_pass_one_on_are_rebuilt_as_before and Constraints_on_one_table_swap_names_and_pass_one_on.
  • A rename with no collision is still one RENAME CONSTRAINT.
  • A chain across two tables whose renames run in an order that frees each name first stays two renames. In the other order, the rename that would wait is rebuilt.

Tests

  • PgExclusionMigrationTests, the statements of each case:
    • an explicit name reused by an index, with btree_gist ensured for the rebuilt constraint
    • a default name reused after a column rename
    • an index renamed into the freed name
    • the mirror cases
    • a table rename that must not hide a collision
    • a move to another schema: no collision where the table was, a collision where it goes
    • a swap and a chain across two tables
    • a swap and a chain on one table, unchanged
  • Integration/PgExclusionLiveTests. Each case is applied through AssertMigratesTo against the catalog of EnsureCreated(), Up and Down, on PostgreSQL 16 and 18. The schema-move cases run Up only: a move back to a table without a schema fails on Npgsql today, which is A table moved back to the default schema stays where it was on Npgsql, and a statement written after the move fails with 42P01 #105.
  • Kernel, Migrations/RenameInPlaceTests, with the sample contributors on Npgsql, SQL Server and SQLite. One case is not on SQLite: a lost holder needs an altered column, and SQLite rebuilds the table for that. The cases:
    • a rename without a collision
    • EF Core taking the old name
    • EF Core giving up the new name
    • EF Core taking the new name first
    • a dropped holder
    • a lost holder
    • a BeforeTables rename that collides is refused; without a collision it stays a rename

I broke each part of the rule in a scratch copy (rsync without .git, bin, obj) and ran both suites against PostgreSQL 18:

Broken Tests that fail
No rule: every in-place statement written as before 9 kernel, 13 PostgreSQL (every rebuild case, statements and live, both cross-table cases)
Claims compared under the source's names (Before) instead of after EF Core's operations A_move_to_another_schema_is_compared_where_the_table_is_by_then, A_move_to_another_schema_neither_invents_a_collision_nor_hides_one
Rebuilt change's statements still written 6 kernel, 13 PostgreSQL
Held names not updated after a rename or a rebuild Constraints_on_two_tables_that_swap_names_are_renamed_once_and_rebuilt_once, A_rename_chain_over_two_tables_is_renamed_where_the_order_allows
Dropped declarations counted as holding their names A_rename_into_a_name_a_dropped_declaration_gives_up_stays_in_place (3 providers)
Lost declarations counted as holding their names A_rename_into_a_name_a_lost_declaration_gives_up_stays_in_place (Npgsql, SQL Server)
No refusal for BeforeTables What_tables_are_made_of_is_not_rebuilt_around_a_collision (3 providers)
Only the name taken compared 9 kernel, 10 PostgreSQL
Only the name given up compared the two cross-table tests

Every suite passes:

  • kernel: 648 tests, on PostgreSQL 18 and 16
  • PostgreSQL package: 222 tests, on PostgreSQL 18 and 16
  • SchemaKit.ApiProbes: 125 tests

dotnet build CodoMetis.EFCore.SchemaKit.slnx has no warning.

What the point fix leaves to #115

It reasons about one moment, the changes in place, and treats every other phase as a whole. It does not look at:

  • A name a create takes against EF Core's operations. NameClaims.Require covers the end state of the consumer's own model only, so a snapshot-to-snapshot diff is not checked.
  • The order among drops, or among creates.
  • A BeforeTables create against an EnsureSchema of the same migration. This is the lead in Change planning and impact: SchemaChange, BaseChange, ImpactOf, dependencies #24's comment.
  • Names that SQLite's generator moves, since it puts a table rebuild at the end of the migration.
  • A rename that frees a name an earlier change in place still needs, when the order could be rearranged instead of rebuilt. The point fix rebuilds the later change; it never reorders.

The ledger over ordered actions is the general answer.

Decisions in this PR that are not confirmed

  • The direction from review: a rename becomes a rebuild, with no temporary names.
  • The mirror case stays a rename. It goes by the moment the rename runs: a name EF Core gives up is not a collision, which is narrower than "the name it takes was held by anything else in the source". The server agrees, and the tests hold it.
  • Other declarations are compared at that moment, not as the whole source and target. A rename into the name of a declaration that is dropped or lost in the same migration stays a rename. A declaration created in the same migration under the freed name does not count, because creates run after.
  • The order the changes in place run decides which of two colliding renames is rebuilt. It is the target's order, as before. A cross-table swap becomes one rename and one rebuild.
  • A BeforeTables rename that collides is refused, with a message, instead of being rebuilt. No shipped contributor is BeforeTables.
  • The sample TestDomainContributor gains ClaimsOverride in the test harness. Domains claim nothing by default, as before.

🤖 Generated with Claude Code

A contributor's change in place runs after EF Core's operations. An index
EF Core created under the name a constraint gave up ran while the
constraint still held it, and the migration failed with 42P07; two
constraints on two tables of one schema swapping names failed the same way.

The planner now compares the name a rename gives up and the name it takes,
through NameClaims, with what holds a name at that moment: EF Core's
objects as the target has them, the declared objects as EF Core's
operations leave them but for those dropped before or lost, and the
earlier changes in place under their new names. A change that collides is
rebuilt: dropped before EF Core's operations and created after them, with
its extension ensured. The contributor's statements for it never reach the
migration. A name EF Core gives up is free by then, as the server confirms.

Closes #99

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 11.0.0 milestone Nov 14, 2026
@CaffeinatedCoder CaffeinatedCoder added bug Something isn't working package/core Affects CodoMetis.EFCore.SchemaKit (the kernel) package/postgresql Affects CodoMetis.EFCore.SchemaKit.PostgreSQL severity/medium Inconsistency or failure in a narrow scenario area/migrations The differ and generator decorators, snapshot format, EnsureCreated and the UseSchemaKit wiring labels Nov 14, 2026
@CaffeinatedCoder
CaffeinatedCoder merged commit 541c901 into main Nov 14, 2026
7 of 8 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/rename-frees-name-late branch November 14, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/migrations The differ and generator decorators, snapshot format, EnsureCreated and the UseSchemaKit wiring bug Something isn't working package/core Affects CodoMetis.EFCore.SchemaKit (the kernel) package/postgresql Affects CodoMetis.EFCore.SchemaKit.PostgreSQL severity/medium Inconsistency or failure in a narrow scenario

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A constraint renamed in place frees its name after EF Core's operations: an index created under that name in the same migration fails with 42P07

1 participant