Skip to content

TPC base with two partial indexes or exclusion constraints over the same parts: no name passes on PostgreSQL or SQLite #167

Closed

Description

@CaffeinatedCoder

Found by a pre-release sweep of main @ eff80b4 (lens: new-check-vs-invariant); reproduced in a scratch test. Holds the tag: a model 5.4.1 accepted has no input that passes on 5.5.0, and the fix decides a default-name answer the snapshot then fixes.

Problem

On a TPC base type with derived tables, two declarations over the same parts with different filters can't be named in any way that passes. This affects coexisting partial indexes, and exclusion constraints such as "no overlap among active rows" plus "no overlap among revoked rows". It hits PostgreSQL and SQLite, where index names are unique per schema or database.

  • Both named: refused at migrations add by TpcDeclaration.RepeatedName, reached from ValidateUniqueIndexNames (CustomMigrationsModelDiffer.cs:1001-1003) and, for constraints, ValidateNamesAcrossKinds (NpgsqlComplexIndexMigrationsModelDiffer.cs:473). The message says "… each time under the same explicit name … Leave the name out".
  • Both unnamed, as that message advises: refused at the declaration by ComplexIndexStorage.AddOrReplace (ComplexIndexStorage.cs:30-37) and NpgsqlExclusionConstraintExtensions.Store (:104). The message says "Two indexes over the same parts (Code) with different filters must both have explicit index names".
  • One named and one not: refused at the declaration as well, because the check is d.IndexName is null || definition.IndexName is null.

The only way out is a schema per derived table, and that works on PostgreSQL only. SQL Server passes, since its index names are unique per table.

This is the case #145's fix left open. #145 shortened the default names of copies and kept telling explicit names to leave the name out, but coexisting partials can't leave it out.

Model

modelBuilder.Entity<Booking>(b =>
{
    b.UseTpcMappingStrategy();
    b.ToTable("bookings");
    b.HasComplexIndex(x => x.Code, isUnique: true, filter: "{RoomId} > 0",  indexName: "ix_code_active");
    b.HasComplexIndex(x => x.Code, isUnique: true, filter: "{RoomId} <= 0", indexName: "ix_code_archived");
    // and likewise:
    // b.HasExclusionConstraint(x => x.RoomId, x => x.During, filter: "{Code} <> 'revoked'", name: "ex_active");
    // b.HasExclusionConstraint(x => x.RoomId, x => x.During, filter: "{Code} = 'revoked'",  name: "ex_revoked");
});
modelBuilder.Entity<SpecialBooking>().ToTable("special_bookings");
modelBuilder.Entity<VipBooking>().ToTable("vip_bookings");

Results on eff80b4, from the TpcHierarchyTests contexts:

Case Result
Named, SQLite InvalidOperationException: The complex index 'ix_code_active' is declared on 'Booking', a TPC type, … each time under the same explicit name. Index names must be unique across the database here … Leave the name out
Named, Npgsql the same, with "unique per schema here"
Named, SqlServer passes, 6 creates
Named exclusion constraints The exclusion constraint 'ex_active' is declared on 'Booking', a TPC type, … Leave the name out
Unnamed ArgumentException: Two indexes over the same parts (Code) with different filters must both have explicit index names

At v5.4.1 the named form diffs cleanly: both indexes and both constraints are created on bookings only, a scratch test there confirmed it. A consumer with this model can't run the first migrations add after upgrading.

Change

Give the copies of an explicitly named declaration a name per table. One way: keep the explicit name on the declaring type's table and give each derived copy the explicit name plus its table, shortened as #145 shortens default names. Another: let coexisting partials be unnamed on a TPC type and make their default names distinct per filter. Whichever form becomes the answer, RepeatedName's advice has to name it, and the declaration-time rule has to accept it. The answer becomes part of the snapshot, so it is cheapest to decide before 5.5.0 ships.

Test

For the model above, Diff(provider, null, model) gives 6 creates on SQLite and PostgreSQL and 6 EXCLUDE statements, each under a name that is distinct per table. Add a guard that the advised form is accepted at the declaration. verify-the-guard applies to both.

Activity

  1. added this to the 5.5.0 milestone on Oct 20, 2026
  2. added
    bugSomething isn't working
    breakingChanges an answer or a signature — needs a major
    severity/highWrong results, crash or failed writes in a realistic scenario
    package/coreAffects EFCore.ComplexIndexes
    package/postgresqlAffects EFCore.ComplexIndexes.PostgreSQL
    area/indexesComplex-property, composite, unique and filtered indexes
    area/constraintsTemporal (WITHOUT OVERLAPS, PERIOD) and exclusion constraints
    area/migrationsThe differs, snapshot format, EnsureCreated and design-time wiring
    on Oct 20, 2026
  3. CaffeinatedCoder commented on Oct 20, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Decided in #176. An explicit name stays on the declaring type's table. Where index names are unique per schema or database (PostgreSQL, SQLite), each other table's copy takes {name}_{table}, shortened past the identifier limit as #145 shortens. On SQL Server the name repeats per table, as EF Core 10 repeats its own HasIndex name; measured: that repetition fails on SQLite with "index … already exists". A temporal foreign key keeps its name on every table. A 5.4.1 snapshot keeps the base's names, so the first migrations add only creates the copies. RepeatedName is gone: no explicit name repeats within a namespace now.

  4. added a commit that references this issue on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/constraintsTemporal (WITHOUT OVERLAPS, PERIOD) and exclusion constraintsarea/indexesComplex-property, composite, unique and filtered indexesarea/migrationsThe differs, snapshot format, EnsureCreated and design-time wiringbreakingChanges an answer or a signature — needs a majorbugSomething isn't workingpackage/coreAffects EFCore.ComplexIndexespackage/postgresqlAffects EFCore.ComplexIndexes.PostgreSQLseverity/highWrong results, crash or failed writes in a realistic scenario

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions