Repository navigation
Don't treat a filtered index as covering a foreign key index - #39652
Open
ajcvickers wants to merge 1 commit into
Open
ajcvickers wants to merge 1 commit into
ajcvickers wants to merge 1 commit into
Conversation
Fixes dotnet#24237 ForeignKeyIndexConvention removed, or declined to create, the index on a foreign key's properties whenever another index started with the same columns. A filtered index only contains the rows that satisfy its filter, so it can't stand in for the foreign key index. ForeignKeyIndexConvention now asks a new protected virtual IsPartial hook whether an index only includes some of the rows. A partial index never covers a foreign key index. ProcessIndexCoverageChanged re-evaluates an index when that changes: foreign key indexes are created when an index becomes partial, and covered indexes are removed when it stops being partial. The new RelationalForeignKeyIndexConvention replaces the core convention for relational providers. It treats an index as partial when it has a filter that wasn't set by convention, and it reacts to the filter annotation changing, since HasFilter runs after the index has been added. It also re-checks partial indexes when the model is finalized, because changing a filter from convention to explicit with the same text doesn't raise an annotation change. Filters set by convention, such as the one SQL Server adds to unique indexes over nullable columns, are deliberately ignored so that existing models don't gain new indexes. An unnamed filtered index on exactly the foreign key's properties is still the foreign key's index, because the model can't hold two unnamed indexes on the same properties. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ajcvickers
marked this pull request as draft
November 14, 2026 11:34
ajcvickers
marked this pull request as ready for review
November 14, 2026 18:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #24237
Problem
ForeignKeyIndexConventiondoesn't create an index on a foreign key's properties when another index already starts with the same columns, and it removes the foreign key's index when such an index is added later. That is only correct if the other index contains every row. A filtered index contains just the rows that satisfy its filter, so a filtered composite index whose leading columns are a foreign key's columns made the foreign key's own index disappear from the model, and from the next migration:The convention couldn't see the filter because filters are a relational concept, and
HasFilterruns after the index has been added, so even a relational check made when the index is added would be too early.Fix
Core.
ForeignKeyIndexConventionhas a new protected virtualIsPartial(IConventionIndex)hook (defaultfalse). A partial index never counts as covering a foreign key index. This applies everywhere the convention decides whether an index is covered: creating a foreign key index, adding or removing an index, changing index or foreign key uniqueness, changing the base type, and the redundant-index log message. A new protected virtualProcessIndexCoverageChangedre-evaluates an index whose coverage changed. Foreign key indexes are created when an index becomes partial, and covered indexes are removed when it stops being partial.Relational. The new
RelationalForeignKeyIndexConventionreplaces the core convention for relational providers.IIndexAnnotationChangedConvention, so adding, changing or removing a filter after the index exists adds or removes the foreign key index.Decisions to look at
[A] IS NOT NULL AND [B] IS NOT NULLto unique indexes over nullable columns. Strictly that filter also excludes rows with a null in a later column that a lookup on a prefix of the columns needs. Counting it would add a new index to every existing SQL Server model with such an index, so these filters are treated as covering, as they always have been. This is documented on the class.IsPartiallooks at the filter on the index itself, not theGetFilter(StoreObjectIdentifier)fallback to the root index in a shared table.Behavior change
A model with an explicitly filtered index whose leading columns are a foreign key's columns will get a new
CreateIndexfor the foreign key in its next migration. This is the intended fix, but it is visible to existing apps and may deserve a breaking-change note.CompiledModelTestBase.Tpc_Sprocshas a model like this, a named unique index filtered onAlternateId <> NULLover the foreign key column. Its assertions and generated baselines now include the foreign key's ownIX_PrincipalBase_PrincipalBaseIdindex.API
New public API, with the two
*.baseline.jsonfiles updated:ForeignKeyIndexConvention.IsPartialandForeignKeyIndexConvention.ProcessIndexCoverageChanged(protected virtual).RelationalForeignKeyIndexConvention.Tests
RelationalForeignKeyIndexFilterTest(22 tests): filtered or unfiltered index created before or after the foreign key; filter added, changed and removed; filter source Convention, DataAnnotation and Explicit (including the same-text upgrade); named and unnamed indexes on exactly the foreign key columns, with unique and non-unique foreign keys; uniqueness changes; index removal; a hierarchy with the index and foreign key on different types, and setting the base type; and the redundant-index log message.ForeignKeyIndexConventionTest(core): theIsPartialhook with a test convention, independent of relational.test.cmdpasses locally.Not covered: filtered indexes over complex properties or collections, and table-specific filters.
🤖 Generated with Claude Code