Skip to content

Document PostgreSQL 18 temporal keys over a range column: PRIMARY KEY … WITHOUT OVERLAPS and FOREIGN KEY … PERIOD #97

Closed

Description

@CaffeinatedCoder

PostgreSQL 18 adds temporal keys over a range column: PRIMARY KEY (id, during WITHOUT OVERLAPS) / UNIQUE (… WITHOUT OVERLAPS) and FOREIGN KEY (room_id, PERIOD during) REFERENCES room (id, PERIOD during) (release notes, https://www.postgresql.org/docs/18/release-18.html; CREATE TABLE, https://www.postgresql.org/docs/18/sql-createtable.html). They are the built-in replacement for the exclusion-constraint recipe docs/efcore.md#reject-overlapping-rows-with-an-exclusion-constraint documents through a third-party package, and they add referential integrity across time, which an exclusion constraint cannot give.

Verified live on 18.4 with btree_gist installed: both constraints create over a daterange column; inserting (1, [2026-01-15,2026-03-01)) next to (1, [2026-01-01,2026-02-01)) fails with 23P01 conflicting key value violates exclusion constraint "room_pkey".

The EF side. Npgsql EF provider 11 (rc as of 2026-10-17) adds the model-building API: HasKey(…).HasWithoutOverlaps() and HasForeignKey(…).HasPrincipalKey(…).HasPeriod(), documented with NpgsqlRange<DateTime> properties (https://www.npgsql.org/efcore/release-notes/11.0.html, "Support for PostgreSQL 18 temporal constraints"). Npgsql 10, the current pin, has nothing, so a migration needs migrationBuilder.Sql(…) and EnsureCreated() cannot create it, as the docs already say for exclusion constraints.

Scope since 2026-10-18 (two lines, epic #147): this issue is the 10.x half.

On the 10.x line (Npgsql 10) the migration no longer needs migrationBuilder.Sql(…): EFCore.ComplexIndexes.PostgreSQL (same author) exposes HasTemporalConstraint, which accepts any period whose store type ends in range, and every mapping of this package carries that name. The docs/efcore.md section is written against that API, next to the existing exclusion-constraint section, measured the same way (the rejected overlap, the accepted adjacent pair, the foreign-key violation, the DbUpdateException shape, the btree_gist requirement). The measured proof lives in #154 (cross-tests with ComplexIndexes); this issue holds the documentation and its claims.

The Npgsql 11 half, native HasWithoutOverlaps() / HasPeriod() over this package's types, is #150 under the 11.0.0 epic and is the preferred form on that line.

No helper belongs in this package on either line: the API is the provider's, or ComplexIndexes'.

Done when: the 10.x section exists with every statement measured against PostgreSQL 18, and claims.json holds its promises (held by #154's tests).

Found by the PostgreSQL catalogue sweep (18 vs 17 diff), 2026-10-17.

Activity

  1. added
    documentationImprovements or additions to documentation
    area/rangesRange unions, variants and the shape engines
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    on Oct 17, 2026
  2. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Rescoped 2026-10-18 for the two-line plan (epic #147):

    • The Npgsql 10 half no longer needs migrationBuilder.Sql(…): EFCore.ComplexIndexes.PostgreSQL (same author) exposes HasTemporalConstraint, and it accepts any period whose store type ends in range, which every mapping of this package carries. This issue keeps the docs/efcore.md section for the 10.x line, written against that API next to the existing exclusion-constraint section; the measured proof lives in the cross-test issue under the backlog ("Cross-tests with EFCore.ComplexIndexes").
    • The Npgsql 11 half moved to Document native temporal constraints over ValueRanges columns on the 11 line: HasWithoutOverlaps and HasPeriod #150 under the 11.0.0 epic: native HasWithoutOverlaps() / HasPeriod() over this package's types, preferred on that line.

    The sentence "No helper belongs in this package: the API is the provider's" still holds on both lines.

  3. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Measured in #159 (ComplexIndexesCrossTests, PostgreSQL 18.x alpine, EFCore.ComplexIndexes.PostgreSQL 5.4.0, Npgsql EF 10.0.0), for the 10.x section this issue writes:

    • HasTemporalConstraint(b => b.RoomId, b => <col>, name) over a DateRange, a RangeSet<DateRange, DateOnly> and a TimeRange (custom timerange) renders UNIQUE ("RoomId", "<col>" WITHOUT OVERLAPS), and pg_get_constraintdef reads back the same text. btree_gist is injected by that package's differ.
    • An overlap in one column is a DbUpdateException wrapping PostgresException 23P01, with ConstraintName set to that constraint. Adjacent rows (including adjacent multiranges) and rows for another key are saved.
    • EnsureCreated() does create these and the exclusion constraints once UseNpgsqlComplexIndexes() is on the options (it registers the package's differ for the runtime path, since 5.1.0). Without it there are none, and overlaps are saved. The current box in docs/efcore.md#reject-overlapping-rows-with-an-exclusion-constraint gives only "Use migrations, or add the constraint with explicit SQL". The section should name the registration as the third way.

    When the section lands, its sentences can name ComplexIndexesCrossTests in heldBy; E40/E41 already hold the exclusion and temporal measurements.

  4. added this to the 10.0.0 milestone on Oct 18, 2026
  5. added
    testsAdds or changes tests only (oracles, harnesses); no library code
    and removed
    testsAdds or changes tests only (oracles, harnesses); no library code
    on Oct 18, 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/rangesRange unions, variants and the shape enginesdocumentationImprovements or additions to documentationpackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions