Skip to content

Native temporal keys over ValueRanges columns on the 11 line: implement INpgsqlRangeTypeMapping, document WithoutOverlaps and WithPeriod #150

Closed

Description

@CaffeinatedCoder

Scope widened 2026-10-19

The measurement (comment below) showed that Npgsql 11 refuses this package's columns as they are, because its validator checks the type mapping's class or interface, not the store type name.

On Npgsql rc.2, implementing INpgsqlRangeTypeMapping on the mappings makes the native API accept them. That is now part of this issue, so it is an enhancement, not only documentation.

Change

  • Floors: Npgsql.EntityFrameworkCore.PostgreSQL (and its NodaTime plugin) move to 11.0.0-rc.2, and EF Core Design in the test projects to rc.2. The interface doesn't exist on rc.1.1. This takes the floor bullet of Publish 11.0.0-preview.1 on EF Core 11 / Npgsql 11 rc.2 #332.
  • ValueRangeTypeMapping, ValueRangeSetTypeMapping and the two YearMonth mappings implement INpgsqlRangeTypeMapping explicitly. CreateCurrentValueComparer returns null, because every range type and RangeSet is already IComparable<T> in PostgreSQL's order.
  • docs/efcore.md gets a section on native temporal keys on the 11 line: HasKey(…).WithoutOverlaps(), WithPeriod() foreign keys, SetPostgresVersion(18, 0), btree_gist, and the DbUpdateException shapes.

Done when

Measured on PostgreSQL 18 in the integration suite:

  • a DateRange key with WITHOUT OVERLAPS, a RangeSet (multirange) key and a TimeRange key over the custom timerange
  • the rejected overlap (23P01) and the accepted adjacent pair
  • a PERIOD foreign key and its violation (23503)
  • the PostgreSQL 18 requirement (SetPostgresVersion) and btree_gist
  • migrations scaffolding the clause
  • a test that removes the interface and shows the refusal returns
  • claims.json holds the promises

Original issue

The second half of #97, split out because it belongs to the 11 line only. Npgsql EF 11 adds HasKey(…).WithoutOverlaps() and HasOne(…).WithMany(…).HasForeignKey(…).WithPeriod() (release notes 11.0, "Support for PostgreSQL 18 temporal constraints"), documented with NpgsqlRange<T>. The provider may check the key column's store type for a range; this package's mappings carry the store type name, so it should pass. It must be measured.

On the 10.x line the same section points to EFCore.ComplexIndexes.PostgreSQL's HasTemporalConstraint (#97). On 11 the native API is preferred, and ComplexIndexes' successor SchemaKit remains for exclusion constraints and GiST indexes over range columns (CaffeinatedCoder/EFCore.ComplexIndexes#37).

Activity

  1. added this to the 11.0.0 milestone on Oct 18, 2026
  2. added
    documentationImprovements or additions to documentation
    area/rangesRange unions, variants and the shape engines
    on Oct 18, 2026
  3. CaffeinatedCoder commented on Oct 19, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Added 2026-10-19: this measurement now decides CaffeinatedCoder/EFCore.ComplexIndexes#39. If Npgsql 11's HasWithoutOverlaps/HasPeriod refuses a period that isn't an NpgsqlRange<T>, then SchemaKit (the EF Core 11 successor of EFCore.ComplexIndexes) keeps HasTemporalConstraint for ValueRanges' types. Otherwise it leaves temporal constraints to the platform, and #333 moves claims E48–E52 to the tests written here. Measure it early; it's on the critical path of both repositories.

  4. CaffeinatedCoder commented on Oct 19, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Measured 2026-10-19 on PostgreSQL 18 (postgres:18-alpine)

    Short answer: Npgsql 11 refuses ValueRanges columns as they are today, on rc.1.1 and on rc.2. On rc.2 a one-line change in ValueRanges makes the native API accept all three types, the multirange included.

    What Npgsql checks

    The native API is HasKey(…).WithoutOverlaps() and HasOne(…).WithMany(…).HasForeignKey(…).WithPeriod(). The issue's HasWithoutOverlaps/HasPeriod don't exist.

    NpgsqlModelValidator checks the last key property's type mapping, not the store type name:

    • rc.1.1: typeMapping is not NpgsqlRangeTypeMapping, which accepts Npgsql's own range mappings only.
    • rc.2: typeMapping is not INpgsqlRangeTypeMapping, an interface introduced for NodaTime's ranges (Support NodaTime ranges in temporal constraints npgsql/efcore.pg#3975). It has one member, IComparer? CreateCurrentValueComparer(Type), which supplies a key comparer for types that aren't IComparable.

    The check also requires SetPostgresVersion(18, 0). Without it the model throws WITHOUT OVERLAPS … requires PostgreSQL 18.0 or later.

    Results

    Period column rc.1.1 / rc.2 as is rc.2 + INpgsqlRangeTypeMapping on the ValueRanges mappings
    NpgsqlRange<DateOnly> (Npgsql's own, baseline) PRIMARY KEY ("RoomId", "Period" WITHOUT OVERLAPS); overlap → 23P01 same
    DateRange (daterange) InvalidOperationException: WITHOUT OVERLAPS … requires the last column to be a PostgreSQL range type …, but property 'Period' has type 'DateRange' renders; adjacent accepted; overlap → DbUpdateException / 23P01 on PK_Slots; rows read back
    TimeRange (custom timerange) same refusal renders (with btree_gist); same results
    RangeSet<DateRange, DateOnly> (datemultirange) same refusal renders; same results
    NpgsqlRange<DateOnly>[] (Npgsql's own multirange) refused refused (Npgsql's multirange mapping doesn't implement the interface)
    PERIOD FK over DateRange PERIOD on foreign key … requires the last column to be a PostgreSQL range type FOREIGN KEY ("RoomId", PERIOD "Period") REFERENCES "Principals" ("RoomId", PERIOD "Period"); a period covered by two adjacent principals is accepted; an uncovered one → 23503

    PostgreSQL 18 itself accepts all of it. Hand-written PRIMARY KEY (room, days WITHOUT OVERLAPS) over datemultirange and over timerange rejects overlaps with 23P01.

    A table created that way, with rows written through EF using an ordinary key over DateRange, rejects the overlap too (23P01). That is the workaround on rc.1.1.

    The change that makes it work

    ValueRangeTypeMapping and ValueRangeSetTypeMapping implement INpgsqlRangeTypeMapping explicitly and return null from CreateCurrentValueComparer. Every range type and RangeSet is already IComparable<T> in PostgreSQL's order, so EF's default comparer applies and Npgsql's convention adds nothing.

    The interface is only read in three places on rc.2: the validator and the two conventions that set that comparer. So the change alters no query translation. It needs the Npgsql floor at rc.2 (#332).

    Consequence for CaffeinatedCoder/EFCore.ComplexIndexes#39

    With this change in ValueRanges 11, Npgsql's native temporal keys cover ValueRanges' DateRange, TimeRange and RangeSet (multirange) columns, and their PERIOD foreign keys. SchemaKit doesn't need to keep HasTemporalConstraint for ValueRanges' sake.

    It still needs it for an NpgsqlRange<T>[] multirange period, which Npgsql refuses. Whether that is reason enough is #39's call.

  5. added
    enhancementNew feature or request
    and removed
    documentationImprovements or additions to documentation
    on Oct 19, 2026
  6. changed the title [-]Document native temporal constraints over ValueRanges columns on the 11 line: HasWithoutOverlaps and HasPeriod[/-] [+]Native temporal keys over ValueRanges columns on the 11 line: implement INpgsqlRangeTypeMapping, document WithoutOverlaps and WithPeriod[/+] on Oct 19, 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/range-setRangeSet and multirange algebraarea/rangesRange unions, variants and the shape enginesenhancementNew feature or requestpackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions