Repository navigation
SchemaKit: leave out what EF Core 11 and Npgsql 11 do natively; keep what stays unique #39
Description
Decided 2026-10-19 (temporal half): leave out. SchemaKit ships no HasTemporalConstraint and no temporal foreign key. Npgsql 11's HasKey(…).WithoutOverlaps() and WithPeriod() cover NpgsqlRange<T> and, since CaffeinatedCoder/CodoMetis.ValueRanges#335, every ValueRanges range and multirange column (measured, comment below). The one gap is Npgsql's own NpgsqlRange<T>[] multirange, which Npgsql rejects; that is Npgsql's to close, and the migration guide names it. Still open here: the HasComplexIndex half (plain, composite, unique, filtered).
Rewritten 2026-10-19 for the spin-off (#37). SchemaKit is a new package, so nothing is delegated or kept as [Obsolete]: each feature is either left out or kept.
On EF Core 11 the platform does two things this package was built for:
HasIndex/HasKey/HasAlternateKeyover complex-type properties, JSON-mapped paths and complex-collection paths (Allow keys and indexes to use properties on non-collection complex types dotnet/efcore#31246, preview.6)- on Npgsql 11,
HasKey(…).WithoutOverlaps()andHasOne(…).WithMany(…).HasForeignKey(…).WithPeriod()(efcore.pg#3708, #3710)
For each of HasComplexIndex (plain, composite, unique, filtered) and HasTemporalConstraint / temporal foreign keys, decide and record here, with a measured SQL diff against EFCore.ComplexIndexes 5.x:
- Leave out: the native API renders the same DDL. The migration guide shows the before and after, and SchemaKit: moving from EFCore.ComplexIndexes 5.x adds no migration, or exactly the one the guide documents #59 proves the swap adds no migration.
- Keep: the native API can't express it (filtered temporal constraints, exclusion constraints,
NULLSordering, expression and JSON-member indexes, the SQL Server options), so SchemaKit stays the only way.
The temporal half waits for CaffeinatedCoder/CodoMetis.ValueRanges#150. It measures Npgsql 11's WithoutOverlaps/WithPeriod over ValueRanges' DateRange, RangeSet (multirange) and custom timerange columns on PostgreSQL 18. Npgsql documents the native API with NpgsqlRange<T>. If it refuses a period that isn't one, HasTemporalConstraint stays in SchemaKit for every other range CLR type.
Everything left out goes into the 11.0.0 changelog's list of what can break you. Nothing here touches the 5.x line.
Activity
[-]Delegate to native EF Core 11 complex-type indexes and Npgsql 11 temporal constraints; keep what stays unique[/-][+]SchemaKit: leave out what EF Core 11 and Npgsql 11 do natively; keep what stays unique[/+]CaffeinatedCoder commentedon Oct 19, 2026
CaffeinatedCoder/CodoMetis.ValueRanges#150, 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()andHasOne(…).WithMany(…).HasForeignKey(…).WithPeriod(). The issue'sHasWithoutOverlaps/HasPerioddon't exist.NpgsqlModelValidatorchecks the last key property's type mapping, not the store type name:typeMapping is not NpgsqlRangeTypeMapping, which accepts Npgsql's own range mappings only.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'tIComparable.The check also requires
SetPostgresVersion(18, 0). Without it the model throwsWITHOUT OVERLAPS … requires PostgreSQL 18.0 or later.Results
INpgsqlRangeTypeMappingon the ValueRanges mappingsNpgsqlRange<DateOnly>(Npgsql's own, baseline)PRIMARY KEY ("RoomId", "Period" WITHOUT OVERLAPS); overlap → 23P01DateRange(daterange)InvalidOperationException: WITHOUT OVERLAPS … requires the last column to be a PostgreSQL range type …, but property 'Period' has type 'DateRange'DbUpdateException/ 23P01 onPK_Slots; rows read backTimeRange(customtimerange)btree_gist); same resultsRangeSet<DateRange, DateOnly>(datemultirange)NpgsqlRange<DateOnly>[](Npgsql's own multirange)PERIODFK overDateRangePERIOD on foreign key … requires the last column to be a PostgreSQL range typeFOREIGN KEY ("RoomId", PERIOD "Period") REFERENCES "Principals" ("RoomId", PERIOD "Period"); a period covered by two adjacent principals is accepted; an uncovered one → 23503PostgreSQL 18 itself accepts all of it. Hand-written
PRIMARY KEY (room, days WITHOUT OVERLAPS)overdatemultirangeand overtimerangerejects 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
ValueRangeTypeMappingandValueRangeSetTypeMappingimplementINpgsqlRangeTypeMappingexplicitly and returnnullfromCreateCurrentValueComparer. Every range type andRangeSetis alreadyIComparable<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 (CaffeinatedCoder/CodoMetis.ValueRanges#332).
Consequence for this issue
With this change in ValueRanges 11, Npgsql's native temporal keys cover ValueRanges'
DateRange,TimeRangeandRangeSet(multirange) columns, and theirPERIODforeign keys. SchemaKit doesn't need to keepHasTemporalConstraintfor ValueRanges' sake.It still needs it for an
NpgsqlRange<T>[]multirange period, which Npgsql refuses. Whether that is reason enough is #39's call.CaffeinatedCoder commentedon Oct 19, 2026
Maintainer decision 2026-10-19: leave temporal constraints out of SchemaKit (recorded at the top of the issue). The
HasComplexIndexhalf stays open.