You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Update README with PostgreSQL temporal constraint documentation and enhance migration model differ
- Added `WITHOUT OVERLAPS` support to README, including examples and setup instructions.
- Enhanced `NpgsqlComplexIndexMigrationsModelDiffer` with range type validation for temporal constraints.
-**Composite Indexes**: Define multi-column indexes spanning both scalar and nested properties with a single, intuitive expression — with per-column `ASC`/`DESC` ordering via `DbOrder.Asc`/`DbOrder.Desc`
16
16
-**Expression Indexes***(PostgreSQL)*: Index arbitrary SQL expressions such as `lower(email)` or `to_tsvector('english', body)` — including on plain, non-complex entities
17
+
-**Temporal Constraints***(PostgreSQL 18)*: Declare `UNIQUE … WITHOUT OVERLAPS` constraints to guarantee no two rows occupy overlapping time periods — the database enforces scheduling integrity for you
17
18
18
19
| Package | NuGet | Description |
19
20
|---|---|---|
20
21
|**EFCore.ComplexIndexes**|[](https://www.nuget.org/packages/EFCore.ComplexIndexes/)| Core library — single-column, composite, unique, and filtered indexes on complex type properties. Works with any EF Core relational provider. |
21
-
|**EFCore.ComplexIndexes.PostgreSQL**|[](https://www.nuget.org/packages/EFCore.ComplexIndexes.PostgreSQL/)| PostgreSQL extensions via [Npgsql](https://www.npgsql.org/efcore/) — adds GIN, GiST, BRIN, SP-GiST, and Hash index methods, operator classes, covering indexes (`INCLUDE`), concurrent creation, nulls-distinct control, and **expression (functional) indexes**. |
22
+
|**EFCore.ComplexIndexes.PostgreSQL**|[](https://www.nuget.org/packages/EFCore.ComplexIndexes.PostgreSQL/)| PostgreSQL extensions via [Npgsql](https://www.npgsql.org/efcore/) — adds GIN, GiST, BRIN, SP-GiST, and Hash index methods, operator classes, covering indexes (`INCLUDE`), concurrent creation, nulls-distinct control, **expression (functional) indexes**, and **temporal `UNIQUE` constraints (`WITHOUT OVERLAPS`)**. |
22
23
23
24
> **Which package do I need?**
24
25
> Install only the **core** package if you use SQL Server, SQLite, or any provider where the default B-tree index type is sufficient.
> **Roadmap:** the expression API is built on an `IIndexExpression` seam. A future LINQ add-on will let you write `HasExpressionIndex(x => x.Email.ToLower())` and have it translated to SQL — flowing through the exact same pipeline.
> Requires `UseNpgsqlComplexIndexes()` (see [Getting started](#expression-indexes-postgresql--one-time-setup)).
176
+
> Available as an extension on `EntityTypeBuilder<TEntity>`, so it works on any entity — complex or not.
177
+
178
+
PostgreSQL 18 introduced `WITHOUT OVERLAPS` for unique constraints — a long-requested feature for scheduling, booking, and versioning scenarios. Instead of only checking *"is this exact value already present?"*, the database enforces *"no two rows for the same key have overlapping time periods"*.
179
+
180
+
```sql
181
+
ALTERTABLE bookings
182
+
ADD CONSTRAINT ak_bookings_room_period
183
+
UNIQUE (room_id, period WITHOUT OVERLAPS);
184
+
```
185
+
186
+
`HasTemporalConstraint` exposes this as a first-class EF Core API. You supply scalar key columns (the "group" — e.g. a room, a resource, an employee) and a period column (a [PostgreSQL range type](https://www.postgresql.org/docs/current/rangetypes.html) such as `daterange`, `tstzrange`, or `NpgsqlRange<T>`):
187
+
188
+
**Single key column:**
189
+
190
+
```csharp
191
+
builder.HasTemporalConstraint(
192
+
keyColumns: b=>b.RoomId,
193
+
period: b=>b.ValidPeriod);
194
+
// ALTER TABLE "Bookings" ADD CONSTRAINT "AK_Bookings__RoomId_ValidPeriod"
195
+
// UNIQUE ("RoomId", "ValidPeriod" WITHOUT OVERLAPS);
196
+
```
197
+
198
+
**Composite key columns:**
199
+
200
+
```csharp
201
+
builder.HasTemporalConstraint(
202
+
keyColumns: b=>new { b.Facility, b.RoomId },
203
+
period: b=>b.ValidPeriod);
204
+
// UNIQUE ("Facility", "RoomId", "ValidPeriod" WITHOUT OVERLAPS)
205
+
```
206
+
207
+
**Explicit constraint name:**
208
+
209
+
```csharp
210
+
builder.HasTemporalConstraint(
211
+
keyColumns: b=>b.RoomId,
212
+
period: b=>b.ValidPeriod,
213
+
name: "uk_room_no_overlap");
214
+
```
215
+
216
+
#### How the period column is validated
217
+
218
+
The migration differ validates the period property at migration-generation time (`dotnet ef migrations add`). It must be mapped to a PostgreSQL range or multirange store type (anything ending in `range` — e.g. `daterange`, `tstzrange`, `int4multirange`) or have a CLR type of `NpgsqlRange<T>` / a multirange struct from `NpgsqlTypes`. Using an incompatible type such as `string`, `int`, or `DateOnly` throws an `InvalidOperationException`*before* any SQL is generated:
219
+
220
+
```
221
+
The temporal constraint period property 'Start' on entity 'Booking' does not appear to be a range or multirange type. Found CLR type 'DateTime' (store type: 'timestamp with time zone'). Expected NpgsqlRange<T>, a PostgreSQL range/multirange column type, or a store type ending in 'range' (e.g., daterange, int4multirange).
222
+
```
223
+
224
+
The period column stays a plain mapped column — it is deliberately **not** part of an EF key, because EF Core forbids non-comparable range types in primary keys. Use a surrogate or scalar EF primary key for change tracking; the temporal constraint handles the non-overlap guarantee independently.
225
+
226
+
#### `btree_gist` extension
227
+
228
+
Temporal constraints over scalar key columns require the `btree_gist` PostgreSQL extension. The differ injects `CREATE EXTENSION IF NOT EXISTS btree_gist;` automatically when a temporal constraint is first added. You can take explicit control or opt out:
229
+
230
+
```csharp
231
+
// Explicit: declare the extension yourself (Npgsql's own differ handles it)
232
+
modelBuilder.UseBtreeGist();
233
+
234
+
// Opt out: e.g. if the extension is provisioned out-of-band by your DBA
When `UseBtreeGist()` is present, automatic injection backs off to avoid a duplicate `CREATE EXTENSION` statement.
239
+
240
+
#### Idempotency and renames
241
+
242
+
Re-declaring a temporal constraint on the same key + period replaces the previous one. Removing `HasTemporalConstraint` from the model causes the differ to emit a `DROP CONSTRAINT` in the next migration (unless the table itself is being dropped).
243
+
172
244
---
173
245
174
246
The package integrates seamlessly with EF Core's design-time tooling. Apart from the one-time `UseNpgsqlComplexIndexes()` call for expression indexes, there is no additional ceremony — just configure and migrate.
0 commit comments