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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ covering only what changed for that package:
[PostgreSQL](src/EFCore.ComplexIndexes.PostgreSQL/CHANGELOG.md),
[SQL Server](src/EFCore.ComplexIndexes.SqlServer/CHANGELOG.md).

## 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.

## 5.4.0

Fixes to what 5.x already ships, found while testing the packages against EF Core 11 and planning
Expand Down
25 changes: 20 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ them with no base to resolve against, so **every link in them must be an absolut
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.

`ROADMAP.md` at the root is the plan for 5.4.0 and the 6.0 family: the delete guard, the generic
`ROADMAP.md` at the root is the plan for 5.5.0 and the 6.0 family: the delete guard, the generic
descriptor differ, the rename under the `CodoMetis.` prefix, the plugin host and EF Core 11. It sits at the
root on purpose: `DocumentationLinkTests` checks its links, but `DocumentationApiTests` scans only
`docs/`, so the roadmap may name methods that do not exist yet.
Expand Down Expand Up @@ -267,7 +267,7 @@ stored as property annotations) and entity-level `HasComplexIndex(x => x.Complex

Names are validated at **two** levels, because neither alone is sufficient. `AddOrReplace` rejects
an explicit name already used on the same entity (fast feedback at the declaration), and the
differ's `ValidateUniqueIndexNames` rejects duplicate resolved names per table — the only place
differ's `ValidateUniqueIndexNames` rejects duplicate resolved names within the provider's scope (below) — the only place
that sees across the two stores (property-level annotations vs the entity-level list) and knows the
*default* names, which depend on resolved column names. It validates the **target model only**: a
snapshot that already contains a collision must stay diffable, or the model could never be fixed.
Expand All @@ -293,8 +293,23 @@ applied clean), and index names per **schema**, where every primary key, unique,
temporal constraint also owns an index (42P07). A collision is reported only when one party is
this package's; two of EF's own objects are EF's business. It collects into a list, not a set: the
two same-named declarations it exists to find produce identical entries, and a set merged them.
Core's `ValidateUniqueIndexNames` stays per table on purpose — SQL Server scopes index names per
table.

Where an index name must be unique is a provider fact, declared through
`CustomMigrationsModelDiffer.IndexNameScope` and applied by both core checks
(`ValidateUniqueIndexNames`, `ValidateNoNativeIndexNameCollision`): `IndexNameScope.Database` by
default, `IndexNameScope.Schema` in the PostgreSQL satellite, `IndexNameScope.Table` in the SQL
Server one. The default is SQLite's rule, and it has to be *database*, not schema: SQLite's provider
keeps a configured schema in the model but leaves it out of the DDL, so `north.a` and `south.b`
share one namespace (probed 2026-09-22). Until 5.4.1 both checks were per table everywhere, and the
SQLite test "the same name on a different table is not a collision" approved a model that
`sqlite3` rejects with "index … already exists". The widest scope is also the right default for
providers without a satellite: too wide costs a rename the database did not need (MySQL scopes per
table), too narrow a failed apply. On PostgreSQL a clash between two plain indexes is therefore
reported by the core, before `ValidateNamesAcrossKinds` runs; the cross-kind check keeps complex
indexes in its list for the pairs only it sees — keys, constraint-backed indexes, and a table in the
unset default schema against one naming `public`, which the core compares as configured. Each
collision throws once, from whichever check sees it first; do not "deduplicate" by filtering index
pairs out of the cross-kind check, since it is the only net for the `public` case.

### The read model is the differ's reader

Expand Down Expand Up @@ -493,7 +508,7 @@ still skipped silently — an index on those is nothing this package could creat

### Key extension points

- **Adding a new provider**: Subclass `CustomMigrationsModelDiffer` (override `IsForwardedIndexAnnotation`, optionally `ValidateCreateIndexOperation`/`ResolveUnmappedPart`/`QuoteIdentifier`), implement `IDesignTimeServices` to replace the differ, and ship a `.targets` file that injects the attribute (with `ForProvider` set). The PostgreSQL project is the full-featured reference; the SQL Server project is the minimal one (whitelist + validation, no custom SQL generator).
- **Adding a new provider**: Subclass `CustomMigrationsModelDiffer` (override `IsForwardedIndexAnnotation` and `IndexNameScope` unless the provider keeps index names unique across the database, optionally `ValidateCreateIndexOperation`/`ResolveUnmappedPart`/`QuoteIdentifier`), implement `IDesignTimeServices` to replace the differ, and ship a `.targets` file that injects the attribute (with `ForProvider` set). The PostgreSQL project is the full-featured reference; the SQL Server project is the minimal one (whitelist + validation, no custom SQL generator).
- **New index options**: Add constants to `ComplexIndexAnnotations.cs` (or `NpgsqlAnnotations.cs`), expose them via `ComplexIndexBuilder`, and read them in the differ when constructing `CreateIndexOperation`.

### Expression path extraction
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>5.4.0</Version>
<Version>5.4.1</Version>
<Authors>CaffeinatedCoder</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down Expand Up @@ -63,7 +63,7 @@
-->
<PropertyGroup>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageValidationBaselineVersion>5.3.0</PackageValidationBaselineVersion>
<PackageValidationBaselineVersion>5.4.0</PackageValidationBaselineVersion>
</PropertyGroup>

<!--
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,12 @@ Selectors also see through a value converter: for a value object mapped as one c
provided the member's type is the converter's provider type. `x => x.CreatedAt.Year` does not
resolve, and says so.

Index names must be unique per table, and the package enforces it rather than letting the database
reject the migration: reusing a name throws at the declaration, and two declarations that resolve to
the same name — including a property-level and an entity-level index over one column, which share a
default name — throw during `dotnet ef migrations add`. So does a name longer than the provider's
Index names must be unique wherever the provider requires — across the whole database on SQLite,
per schema on PostgreSQL, per table on SQL Server — and the package enforces it rather than letting
the database reject the migration: reusing a name throws at the declaration, and two declarations
that resolve to the same name — including a property-level and an entity-level index over one
column, which share a default name, or a complex and a native index on two SQLite tables — throw
during `dotnet ef migrations add`. So does a name longer than the provider's
identifier limit: PostgreSQL would otherwise truncate it to 63 bytes with a NOTICE and apply the
migration cleanly, leaving the index under a name that no declaration and no constraint-violation
error ever reports. Default names are checked too, since this package never truncates them.
Expand Down
Loading
Loading