Skip to content

Releases: CaffeinatedCoder/EFCore.ComplexIndexes

v5.4.1

Choose a tag to compare

@github-actions github-actions released this 22 Sep 19:46
85a9e2d

5.4.1

One fix, to the index-name check: it compared names per table on every provider, so on SQLite a name
reused on another table scaffolded cleanly and failed when the migration was applied.

  • Fixed: on SQLite, a complex index name is checked across the whole database at migrations add, not per table. SQLite rejects a second CREATE INDEX under a name already used on any other table, and ignores the schemas a model configures, so two tables sharing an index name — two complex indexes, or a complex index and a native HasIndex — scaffolded cleanly and failed when applied ("index … already exists"). The scope is now the provider's: per database in the core, which also serves providers without a satellite; per schema on PostgreSQL; per table on SQL Server, where reusing a name across tables stays allowed. Upgrading: a SQLite model that reuses a complex index name across tables now fails at migrations add, naming both tables; rename one. On a provider without a satellite that scopes names per table, such as MySQL, that rename is one the database would not have needed, and with the differ registered at runtime (UseComplexIndexes()) Migrate() raises the same error until it is made. A snapshot holding such a name stays diffable.

Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.

v5.4.0

Choose a tag to compare

@github-actions github-actions released this 22 Sep 19:15
19c53bb

5.4.0

Fixes to what 5.x already ships, found while testing the packages against EF Core 11 and planning
the next features: indexes on JSON members that no query could use, and constraint names that only
failed — or quietly vanished — when the migration was applied.

  • Fixed: indexes on members of a ToJson() complex property are written the way Npgsql's queries read the member, so PostgreSQL can use them. It uses an expression index only for a query whose expression matches, and until now every member was extracted as "doc" -> 'A' ->> 'B' text with no cast, which matched only a top-level string. A nested member (#>> '{Address,City}' in the query) or a typed one (CAST("doc" ->> 'Rank' AS integer)) got an index that applied cleanly, enforced uniqueness, and was never used by a single query. Members now render as Npgsql renders them — ->> or #>>, cast to the member's store type unless it is a string, decode(…, 'base64') for byte[], jsonb for a primitive collection or a json/jsonb member — in index parts, typed expression indexes and index filters. That also makes a filter such as x => x.Profile.Rank > 5 valid SQL: it compared text with an integer and failed at apply time. Default index names do not change. Upgrading: the first migrations add drops and re-creates each affected index under its existing name; indexes on top-level string members are untouched. Until that migration exists, has-pending-model-changes reports changes and Migrate() raises EF Core's pending-model-changes error. The re-create blocks writes while it builds, so on a large table declare the index with IsCreatedConcurrently() first. Exclusion constraint filters keep their rendering. The snapshot gains one CustomIndex:RenderingVersion annotation, which records the rules a model was declared under and is what lets the change reach databases built by earlier versions.
  • Changed: a non-unique index that starts with a date or time JSON member (DateTime, DateTimeOffset, DateOnly, TimeOnly) is rejected at migrations add. EF Core's queries cast such a member to timestamptz, date or time, and PostgreSQL cannot index those casts (the conversion from text is not IMMUTABLE), so no query could ever use the index. A unique one is still allowed and enforces uniqueness on the stored text; a member in a later position leaves the index usable through the parts before it.
  • Fixed: on PostgreSQL, a name this package introduces is checked against every object it shares a namespace with, at migrations add. PostgreSQL keeps constraint names unique per table and index names unique per schema — including the index behind every primary key, unique, exclusion and temporal constraint — but each kind was checked against its own kind on one table at most. Two temporal constraints with one name, a temporal foreign key named like a foreign key on its table, or a temporal constraint named like an index anywhere in the schema scaffolded cleanly and failed when applied (42710, 42P07). Against an exclusion constraint it did not even fail: every exclusion constraint is added after DROP CONSTRAINT IF EXISTS, so a same-named temporal constraint was dropped and the migration applied clean, one declared guarantee short. Two explicitly named complex indexes on different tables of one schema are caught the same way. A collision between two of EF Core's own objects is left to EF; a snapshot holding a collision stays diffable.

Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.

v5.3.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 10:21
2e28a24

5.3.0

One fix, found the first time a 5.2.0 converter-member path met the model snapshot
it had just been scaffolded into.

  • Fixed: a property path through a value converter (x => x.Email.Value) now resolves against a model snapshot as well as against the configured model. A snapshot persists a converted property as its provider type — string, on a property-bag type — and drops the converter, so the member check that guards x.CreatedAt.Year had nothing to check against and the path failed to resolve. The first dotnet ef migrations add succeeded, because the snapshot did not hold the path yet; everything that diffed the resulting snapshot then threw Could not resolve property path 'Email.Value' — the next migrations add, has-pending-model-changes, and Migrate(), whose pending-model-changes check runs the differ this package registers before applying anything. On a property-bag type the persisted scalar is now accepted for a path the configured model already validated; against a configured model the provider-type check is unchanged. Covers expression templates, column parts, composite parts, filter placeholders and PostgreSQL exclusion elements, at the top level and inside a complex type.

Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.

v5.2.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 08:48
b2aca4f

5.2.0

The features AuditOffice's review of its own workarounds asked for, in the order they pay off:
a validation for a failure that reports nothing, a read model so an application can check its
own obligations, filters that resolve property paths the way index parts already do, an amend
API, and typed filter predicates.

  • Changed: an index or constraint name longer than the provider's identifier limit is rejected at dotnet ef migrations add. PostgreSQL truncates a name past 63 bytes with a NOTICE and applies the migration cleanly, so the index exists under a name that neither the declaration nor a later constraint-violation error reports — a slice dispatching on the constraint name falls through in silence; SQL Server rejects the statement at apply time instead. Explicit and default names alike are checked, measured the way the provider measures them (bytes on PostgreSQL, characters on SQL Server), on the target model only. The names this package derives are never truncated, unlike EF Core's own default names, so a long table name plus a long column path reaches the limit quietly, and a default temporal foreign key name, built from two table names, is the first to. Give the declaration a name; a name-only change renames in place.

  • New: a read model. GetComplexIndexes() on an entity type or the model returns every declaration this package holds — property-level, entity-level, composite and expression indexes alike — as ComplexIndexDeclarations carrying the parts as property paths, IsUnique, Filter, the explicit Name and, for entity-level declarations, the provider options; FindComplexIndex(name) looks one up by explicit name. The PostgreSQL package adds GetExclusionConstraints() and FindExclusionConstraint(name) for exclusion constraints, whose element type ExclusionPartDefinition and annotation key NpgsqlExclusionAnnotations.Constraints are now public. Both work on the mutable model inside OnModelCreating, so an application can check a convention such as "every unique index and exclusion constraint on a withdrawable aggregate is filtered to live rows" while the model is built. The differ builds its own descriptors from the same readers, so what the read model reports is what the migration is scaffolded from.

  • New: filters resolve {Property.Path} placeholders, the way expression parts already did — on complex, composite and expression indexes, and on PostgreSQL exclusion constraints. filter: "{RevokedAt} IS NULL" becomes "revoked_at" IS NULL ([revoked_at] on SQL Server) at migrations add, honouring HasColumnName and, on PostgreSQL, ToJson() members; the resolved text is what the migration carries, rendered by the stock generator with no runtime wiring, and what the snapshot is compared on. Only a brace pair holding a dotted identifier path outside a single-quoted literal is a placeholder, so existing filters with array or JSON literals ('{urgent}', '{"a": 1}') are unaffected, and a placeholder naming no property fails loudly. Template resolution now lives in the core differ over a new QuoteIdentifier seam, which is also why the SQL Server satellite resolves them.

  • New: property paths see through a value converter. For a value object mapped as one column, x => x.Email.Value — in HasComplexIndex, a typed HasExpressionIndex, an exclusion element, or a filter placeholder — resolves to that column, provided the member's type is the converter's provider type. Without that guard x.CreatedAt.Year would have indexed the whole column; it still fails, and says so.

  • New: a mutable API. On an IMutableEntityType, AddComplexIndexFilter(predicate, where) ANDs a predicate onto the filter of every selected complex index — property-level and entity-level alike — so a shared convention can install a live-rows filter the way it installs the query filter, instead of every configuration repeating it; AddComplexIndex(definition) adds an entity-level declaration with the fluent API's identity and name rules. The PostgreSQL package adds AddExclusionConstraintFilter. An unfiltered declaration gets the predicate, a filtered one (existing) AND (predicate), one that already carries it is left alone, so the calls are safe to repeat. They amend what is declared at the time of the call, so they belong after the configurations.

  • New: typed filter predicates on PostgreSQL. Every filter: string has a lambda form — HasComplexIndex(x => x.Email, x => x.RevokedAt == null), HasComplexCompositeIndex(…, x => …), HasExpressionIndex(…, x => …), HasExclusionConstraint(…, x => …) — and the index and constraint builders take HasFilter(x => …) (HasFilter<TEntity> on the non-generic index builders). The predicate is translated at the declaration into a filter template, so the column names come from the model: ==/!= (IS NULL/IS NOT NULL against null), <, <=, >, >=, &&, ||, !, boolean properties, and on the operands the string operations and constants a typed expression index accepts. Enums are refused — how one is stored depends on a value conversion the filter cannot see, so Status == Status.Active would compare a text column against 0 and fail at apply time — as are values with no portable SQL spelling (DateTime, Guid), which the typed expression translator used to render as bare text. The amend calls have typed forms too.

Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.

v5.1.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 06:46
2110a80

5.1.0

Small enhancements around the two seams, plus the silent failures found while planning the next
major: a pending-changes check that never saw this package, and two ways a declaration could vanish
from a migration without a word.

  • Fixed: HasDifferences now reports changes to complex indexes, exclusion constraints and temporal constraints. EF Core's base implementation runs its own Diff rather than the GetDifferences this package overrides, so every check built on it reported "no changes" when only a declaration from this package had changed: dotnet ef migrations has-pending-model-changes, the pending-model-changes warning Migrate() raises since EF Core 9, and the snapshot check in migrations remove. A CI gate built on has-pending-model-changes may now fail where it previously passed — that is the gate working.
  • Fixed: a complex index whose name matches a native HasIndex on the same table is now rejected at dotnet ef migrations add. The base differ emitted one and this package the other, neither seeing the other, so the migration scaffolded two CREATE INDEX statements under one name and failed when applied (42P07). Only the target model's native indexes are consulted, so an index moving between a native declaration and a complex one under the same name still diffs as before.
  • Fixed: a provider option from the other satellite on a property-level complex index is now rejected at migrations add, like an entity-level one, instead of being dropped by the forwarding whitelist without a word — .UseGin() on a property-level index diffed by the SQL Server satellite scaffolded a plain B-tree. The PostgreSQL differ likewise rejects SQL Server options, which it previously passed through to a generator that ignored them.
  • New: runtime registration of the differ. Database.EnsureCreated(), GenerateCreateScript() and the pending-model-changes check Migrate() performs run the context's runtime differ, which the design-time wiring never reaches — so EnsureCreated() created the tables and silently none of the complex indexes, and Migrate() never warned about one that was not scaffolded. UseNpgsqlComplexIndexes() now registers the PostgreSQL differ alongside the generator; SQL Server gets UseSqlServerComplexIndexes(); providers without a satellite get UseComplexIndexes() from the core package. Each has an Add…ComplexIndexes counterpart for a custom internal service provider. With a satellite installed, call the satellite's method only.
  • New: whole-document JSON indexes on PostgreSQL. Pointing HasComplexIndex at a ToJson() complex property — or at a complex collection, which is always JSON — now indexes its jsonb container column, so HasComplexIndex(x => x.Payload, ix => ix.UseGin().HasOperators("jsonb_path_ops")) produces the idiomatic GIN index. Previously the path failed with "could not resolve property path", and complex collections could not be indexed at all. The container is a real column, so no runtime wiring is involved; a complex property nested inside the document resolves to a -> extraction and renders like any expression index.
  • New: HasStorageParameter(name, value) on PostgreSQL complex and expression indexes — WITH (fillfactor=70), WITH (fastupdate=false), and so on. Column indexes render through Npgsql's own generator; expression indexes through this package's, in the same clause position.
  • New: UseCollation(params string[]) on PostgreSQL complex and expression indexes — per-column index collations ("Name" COLLATE "C"), positional, with an empty entry leaving that column on its default. Independent of the column's own collation, which is never copied onto the index.
  • New: the property-level HasComplexIndex overloads also exist on the non-generic ComplexTypePropertyBuilder, so a property configured by name (c.Property("Value")) or by type can carry a complex index.
  • Changed: a complex index, exclusion constraint or temporal constraint declared on an entity type that is mapped to no table — typically the abstract base of a TPC hierarchy — now fails at migrations add instead of producing nothing without a word. Declarations on view-mapped and query-mapped types are still ignored.

Published to nuget.org through Trusted Publishing by this run. The CycloneDX SBOM for each package is attached below.

v5.0.3

Choose a tag to compare

@CaffeinatedCoder CaffeinatedCoder released this 15 Aug 15:04
c472a25

What changed in 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.

v5.0.2

Choose a tag to compare

@CaffeinatedCoder CaffeinatedCoder released this 15 Aug 13:20

What changed in 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.

v5.0.1

Choose a tag to compare

@CaffeinatedCoder CaffeinatedCoder released this 10 Aug 17:51

What changed in 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.

v5.0.0

Choose a tag to compare

@CaffeinatedCoder CaffeinatedCoder released this 10 Aug 16:01

What changed in 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 (see above).
  • 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.

v4.0.0

Choose a tag to compare

@CaffeinatedCoder CaffeinatedCoder released this 14 Jun 18:54

What's New in Version 4.0.0

This release introduces comprehensive support for PostgreSQL 18 Temporal Constraints, allowing you to enforce temporal referential integrity directly at the database level while keeping your EF Core models clean.

  • Temporal UNIQUE Constraints (WITHOUT OVERLAPS)

    • Added the HasTemporalConstraint() builder method to define standalone temporal unique constraints over scalar keys and a period (range) column.
    • Automatically handles the injection of the required btree_gist extension during migrations.
  • Temporal FOREIGN KEY Constraints (PERIOD)

    • Added the HasTemporalForeignKey() builder method to enforce temporal referential integrity between dependent and principal entities.
    • Ensures that a dependent's period is fully covered by matching principal periods.
    • Supports composite keys and includes built-in validation to ensure matching key counts and correct range type usage.

Both features generate proper standalone DDL constraints during EF Core migrations (requires UseNpgsqlComplexIndexes()), bypassing EF's limitation on using non-comparable range types in primary keys.

Full Changelog: v3.1.5...v4.0.0