Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jobs:
run: dotnet build -c Release

# The full suite gates the release, which also means ChangelogConsistencyTests runs:
# a version with no changelog entry in the root or package READMEs fails here.
# a version with no changelog entry in the root or package CHANGELOG.md fails here.
# CI=true keeps the integration layer mandatory.
- name: Test
env:
Expand Down Expand Up @@ -259,12 +259,12 @@ jobs:
# attaching ours would invite a hash comparison that fails for a benign reason. nuget.org is the
# immutable store for the packages; the SBOM has no other home.
#
# The release itself is created here when it does not exist yet, with the README's "What changed"
# section as the notes — the same text ChangelogConsistencyTests already requires for the shipped
# version, so an empty extraction is a workflow bug and fails the job rather than publishing a
# blank release. A release created by hand before the tag was pushed is left as written; only the
# assets are added. Either way the packages are already on nuget.org: a failure here is loud
# and costs nothing but a manual upload.
# The release itself is created here when it does not exist yet, with the root CHANGELOG.md's
# section for this version as the notes — the same text ChangelogConsistencyTests already requires
# for the shipped version, down to the heading style it asserts, so an empty extraction is a
# workflow bug and fails the job rather than publishing a blank release. A release created by hand
# before the tag was pushed is left as written; only the assets are added. Either way the packages
# are already on nuget.org: a failure here is loud and costs nothing but a manual upload.
#
# Separate from `publish` on purpose: that job holds id-token: write, this one holds
# contents: write, and no job holds both.
Expand All @@ -280,7 +280,7 @@ jobs:
contents: write # create the release and upload assets; deliberately no id-token

steps:
# The checkout is for README.md, the source of the release notes.
# The checkout is for CHANGELOG.md, the source of the release notes.
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- name: Download packages
Expand Down Expand Up @@ -310,14 +310,14 @@ jobs:
fi

# The section for this version: from its heading up to, not including, the next H2.
notes="$(awk -v heading="## What changed in $VERSION" '
notes="$(awk -v heading="## $VERSION" '
$0 == heading { found = 1; print; next }
found && /^## / { exit }
found { print }
' README.md)"
' CHANGELOG.md)"

if [[ -z "$notes" ]]; then
echo "::error::README.md has no '## What changed in $VERSION' section to use as release notes."
echo "::error::CHANGELOG.md has no '## $VERSION' section to use as release notes."
exit 1
fi

Expand Down
59 changes: 59 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Changelog

All releases of the three packages, newest first. Each package also carries its own changelog,
covering only what changed for that package:
[core](src/EFCore.ComplexIndexes/CHANGELOG.md),
[PostgreSQL](src/EFCore.ComplexIndexes.PostgreSQL/CHANGELOG.md),
[SQL Server](src/EFCore.ComplexIndexes.SqlServer/CHANGELOG.md).

## 5.0.3

A packaging and documentation release. No behaviour changes to the differ or the generated SQL.

- **Changed:** the EF Core dependency now declares an exclusive upper bound — `[10.0.0, 11.0.0)` on `Microsoft.EntityFrameworkCore.Abstractions` for the core package, and on the provider package for each satellite. This package subclasses `MigrationsModelDiffer` and calls internals EF marks as changeable without notice in any release, so an open-ended `>= 10.0.0` let NuGet resolve a future major where the differ can break — surfacing as a confusing `dotnet ef` failure in your project rather than anywhere visible from here. **Nothing changes for existing consumers:** NuGet resolves the lowest version in a range, so restore still picks 10.0.0. Adopting EF Core 11 will need a release that lifts the ceiling deliberately, once the differ has been tested against it.
- **New:** the public API is now fully documented, so IntelliSense no longer comes up empty on the fluent API, the annotation keys, `CompositeIndexDefinition`, or `IndexPartDefinition`. The shipped `.xml` had 64 holes in it; `TreatWarningsAsErrors` now keeps it complete.
- **Tests:** a consumer smoke test runs on every PR and on release. It packs the packages, installs them into a throwaway project created outside this repository, and runs a real `dotnet ef migrations add` — then asserts on the scaffolded content, because the failure it guards against is a migration that succeeds while silently omitting every index. Nothing previously exercised the delivery chain end to end: NuGet restore, the packaged `.targets` injecting the design-time attribute, EF's host discovering it, and the right differ winning.

## 5.0.2

A review of the 5.0.1 tree turned up eleven issues. The first three produced migrations that
scaffolded *and applied* cleanly while being silently wrong; the rest turn late, obscure, or silent
failures into errors raised at the declaration or during `dotnet ef migrations add`.

- **Fixed:** the design-time differ is now selected deterministically. A satellite package's `DesignTimeServicesReferenceAttribute` is scoped to its provider (`ForProvider`), and the core registration backs off when a satellite is present — previously, because the core package's attribute rides along transitively and EF resolves last-registration-wins, NuGet's restore order decided which differ ran. A solution referencing two satellites could hand one provider's model to the other provider's differ, silently dropping its index options.
- **Fixed:** temporal `UNIQUE … WITHOUT OVERLAPS` constraints and temporal foreign keys are now rendered at design time, like exclusion constraints, and no longer need `UseNpgsqlComplexIndexes()`. Previously a consumer without that wiring got a plain `UNIQUE (key, period)` — valid DDL that applied cleanly and silently dropped the entire non-overlap guarantee. Migrations scaffolded before this change keep working: the SQL generator still renders the old stamped operations.
- **Fixed:** exclusion-constraint identity now includes the filter, so two `EXCLUDE` constraints over the same columns with different predicates coexist (both must be named) instead of the second silently replacing the first — the filtered-overlap case the API exists for. Re-declaring with the same filter still updates in place.
- **Fixed:** duplicate index and exclusion-constraint names are now rejected instead of producing a migration that fails at apply time (42P07) — or, for exclusion constraints, one that applies silently and leaves only the last constraint standing. Reusing an explicit name throws at the declaration; collisions between default names, or between a property-level and an entity-level declaration, throw during `migrations add`.
- **Fixed:** `CompositeIndexDefinition` equality compares array-valued provider annotations (operator classes, INCLUDE lists) by content instead of by reference.
- **Fixed:** index, temporal-constraint, and exclusion-constraint selectors that read a captured variable or static member instead of the lambda parameter (`x => captured.Name`) now throw at the declaration, naming the offending selector — previously they produced an unmatchable property path that failed much later with an opaque resolution error.
- **Fixed:** provider validation no longer inspects index operations this package did not create. The satellites previously swept every `CreateIndexOperation` in the migration, so a plain native `HasIndex` carrying a provider option outside the satellite's whitelist would have failed the entire `migrations add` — harmless with today's providers, but it tied your migrations to the exact index-option set each satellite knows about.
- **Fixed:** `DbOrder.Asc` now marks a column ascending, and combining it with `DbOrder.Desc` (or `NullsFirst` with `NullsLast`) throws instead of silently picking one. Repeating the same marker is still fine.
- **Fixed:** `Npgsql:IndexSortOrder`/`IndexNullSortOrder` are no longer forwarded onto complex indexes, and setting either now throws with a pointer to `DbOrder`. They duplicated what `DbOrder.Asc`/`Desc`/`NullsFirst`/`NullsLast` already express per column, giving one index two sources of truth for its sort options — with the annotation's half silently losing whenever the index rendered through this package's generator.
- **Fixed:** clustered-index combinations SQL Server rejects are now caught at `migrations add` rather than at apply time: a clustered index with `INCLUDE` columns, a clustered filtered index, two clustered complex indexes on one table, and — the common one — a clustered complex index on a table whose primary key already holds the clustered slot, which is the SQL Server default.
- **New:** `UseDataCompression(DataCompressionType)` on SQL Server complex indexes — the annotation was already forwarded but had no way to set it.

## 5.0.1

- **Changed:** exclusion-constraint `ADD CONSTRAINT` DDL is now preceded by `DROP CONSTRAINT IF EXISTS`, so adopting a pre-existing hand-written constraint of the same name applies cleanly instead of failing with `42P07`. The standalone drop path also uses `IF EXISTS`.
- **Fixed:** renaming a table no longer drops and recreates the exclusion and temporal constraints it carries (the same normalization complex indexes already had).
- **Changed:** a name-only change to an exclusion constraint, temporal constraint, or temporal foreign key — including the implicit one when a table rename changes a default-derived name — now emits `ALTER TABLE … RENAME CONSTRAINT` instead of dropping and rebuilding. Dependent temporal foreign keys survive such renames untouched.
- **Tests:** the differ is now exercised against *real* model snapshots — generated as C#, compiled in-memory, and rebuilt exactly as `dotnet ef migrations add` does — guarding the whole feature set against snapshot round-trip churn.

## 5.0.0

- **Fixed:** custom `DROP INDEX` operations are now ordered *before* the base migration operations. Previously, moving an index between a native `HasIndex` and a complex-index declaration scaffolded a migration that created the new index before dropping the same-named old one — colliding at apply time.
- **Fixed:** descending parts of expression indexes now render `DESC` (declarable via `ExpressionIndexBuilder.Descending()`).
- **Fixed:** integral provider-annotation values (e.g. fill factor) survive snapshot round-trips as `int` instead of degrading to `double`, which made generators drop them.
- **Changed:** property annotations are forwarded onto index operations through a provider **whitelist** instead of a blacklist. Column facets such as `Relational:ColumnName` no longer leak into scaffolded migrations, and the class of phantom drop/create churn caused by snapshot/code-model annotation asymmetries is closed for good.
- **Changed:** an indexed property that resolves to no column now throws at `migrations add` instead of silently dropping the index — unless it is a `ToJson()` member, which now resolves to a JSON expression index (PostgreSQL).
- **Changed:** two indexes over the same columns may now coexist when their filters differ (both must be named); re-declaring with the same filter still updates in place.
- **New:** entity-level `HasComplexIndex(x => x.Complex.Prop, …)` for single-column indexes, enabling multiple filtered indexes per column.
- **New:** `HasExclusionConstraint` — `EXCLUDE` constraints with `WHERE` predicates.
- **New:** typed LINQ expression indexes — `HasExpressionIndex(x => x.Email.ToLower())`.
- **New:** JSON member indexes for `ToJson()` complex properties.
- **New:** `NULLS FIRST`/`NULLS LAST` via `DbOrder.NullsFirst/NullsLast` and `ExpressionIndexBuilder.NullsFirst()/NullsLast()` (PostgreSQL).
- **New:** the **EFCore.ComplexIndexes.SqlServer** satellite — clustered, covering, online, fill-factor, and sort-in-tempdb options.
- **Changed:** `IncludeProperties(...)` entries are now resolved as property paths (complex members included) with verbatim column-name fallback — `IncludeProperties("Email.Value")` finds the real column.
- **Changed:** a name-only index change now emits `RenameIndexOperation` (PostgreSQL, SQL Server) instead of dropping and rebuilding the index; the core default remains drop + create for providers that cannot rename standalone.
- **Changed:** renaming a table no longer drops and recreates the complex indexes it carries.
- **Changed:** indexes requiring the custom PostgreSQL generator carry a loud sentinel column, so a missing `UseNpgsqlComplexIndexes()` fails at apply time with an actionable error instead of applying a silently wrong index.
24 changes: 20 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ packs with no `id-token` permission at all — nothing in it can mint a token. `
already produced. So a reviewer is asked after the suite has passed rather than before, no token
exists until they approve, and the artifact published is the one that was tested. `release` runs
last with `contents: write` and no `id-token`: it creates the GitHub release if none exists (notes
taken from the README's `## What changed in <version>` section, so an empty extraction fails the
taken from the root `CHANGELOG.md`'s `## <version>` section, so an empty extraction fails the
job instead of publishing a blank release) and attaches the SBOMs — their only durable home, since
workflow artifacts expire after 90 days and nuget.org has no slot for them. Only the SBOMs are
attached: nuget.org repository-signs packages on ingestion, so a `.nupkg` from there never matches
Expand All @@ -116,7 +116,9 @@ below exist because ordinary review does not catch it.

| Test class | Guards |
|---|---|
| `ChangelogConsistencyTests` | The changelog lives in four files (root README + one per package). Asserts the shipped version is documented, no README runs ahead of `Directory.Build.props`, package changelogs are a subset of the root's, and sections are newest-first. |
| `ChangelogConsistencyTests` | The changelog lives in four files (root `CHANGELOG.md` + one per package). Asserts the shipped version is documented, no changelog runs ahead of `Directory.Build.props`, package changelogs are a subset of the root's, sections are newest-first, and no README has grown a duplicate copy. The `## <version>` heading style is asserted too, not merely parsed: `release.yml` matches it literally to extract the release notes, so a section demoted to `###` would read as documented here while the release job published a blank release. |
| `DocumentationLinkTests` | Relative markdown links resolve to real files and `#anchors` to real headings, and the packed READMEs under `src/` carry **no** relative links at all. nuget.org renders those READMEs with nothing to resolve a relative path against, so `[docs](docs/postgresql-indexes.md)` renders as a live link that 404s for every consumer arriving from the package page — while looking correct in the repository. |
| `DocumentationApiTests` | Every method name the user-facing docs cite exists in the public surface (calls into EF Core, Npgsql, DI and the BCL are an explicit allowlist, so everything else has to be ours), and no provider page cites another provider's *exclusive* API — exclusive meaning after subtracting what core and the other satellite also declare, since `IsUnique`/`HasName`/`IncludeProperties` exist on all three builders. Package validation already fails the pack on a removed public member, so what this adds is the rename fixed in source and forgotten in prose, and the name simply typed wrong. Changelogs are deliberately out of scope: an entry saying 5.0.0 shipped `HasExclusionConstraint` stays true after a later rename, and asserting over them would turn every rename into pressure to rewrite history. |
| `PackagingConventionTests` | Every package ships its own README as `PackageReadmeFile`; `.targets` ship to both `build/` and `buildTransitive/`, reference a real `IDesignTimeServices` in their own assembly, and set `ForProvider` on satellites but not on core. Package validation is enabled and its baseline is the shipped version or the release before it, never older — the baseline is what `dotnet pack` diffs the public surface against (CP0002 on a removed member), and one left behind stops seeing API added since it. |
| `ClaudeMdConsistencyTests` | This file. Prose cannot be asserted, so it checks the falsifiable parts: cited paths and file names exist, annotation keys under a prefix this repo owns are declared somewhere, `Type.Member` references resolve, and the stated size of the Npgsql whitelist matches it. Those are what a rename rots silently — and the count claim had already gone stale by two. |
| `BuilderApiParityTests` | Every key in a satellite's annotation whitelist is reachable from a builder method. `SqlServer:DataCompression` sat whitelisted with no API for a full release; this catches that class of drift by invoking every builder extension and diffing the keys it sets. |
Expand Down Expand Up @@ -189,8 +191,22 @@ This library fills a gap in EF Core 10.0 migrations: EF Core can model complex p
Shipping projects live under `src/`, the test project under `test/`; the `.slnx` groups them into
matching solution folders. Shared NuGet metadata and the package version live in the root
`Directory.Build.props`, which still applies to every project beneath it. Each shipping project
carries its own `README.md`, packed as that package's NuGet landing page — keep the per-package
changelogs in sync with the root `README.md` when releasing.
carries its own `README.md`, packed as that package's NuGet landing page, and its own
`CHANGELOG.md`, which is not packed — keep those in sync with the root `CHANGELOG.md` when
releasing.

### Documentation layout

The root `README.md` is the landing page: what the package is, install, runtime wiring, the
provider-agnostic core API, and a table pointing at the rest. Provider-specific reference lives
under `docs/` (`postgresql-indexes.md`, `postgresql-constraints.md`, `sqlserver.md`) — the split
follows the seam, since PostgreSQL indexes go through the differ plus the custom generator while the
temporal and exclusion constraints are rendered as design-time `SqlOperation`s.

The packed READMEs under `src/` are a separate audience and a separate constraint: nuget.org renders
them with no base to resolve against, so **every link in them must be an absolute GitHub URL**. They
are condensed on purpose and will overlap `docs/` — that duplication is the price of a package page
that stands alone, and `DocumentationLinkTests` guards only the part that fails silently.

### How it works end-to-end

Expand Down
Loading
Loading