Release 5.2.0 - #29
Merged
Merged
Conversation
A complex index, exclusion constraint, temporal constraint or temporal foreign key whose resolved name exceeds the provider's identifier limit now fails at `migrations add`. PostgreSQL truncates such a name to 63 bytes with a NOTICE and applies the migration cleanly, so the object 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. Both are caught here. The limit comes from EF's GetMaxIdentifierLength() (63 on PostgreSQL, 128 on SQL Server, none on SQLite), measured the way the provider measures it: characters in the core, UTF-8 bytes in the PostgreSQL satellite (MeasureIdentifier). Default names are checked too — this package never truncates them, unlike EF's own — and only the target model is validated, so a snapshot carrying such a name stays diffable. The test model for temporal foreign keys was the first to hit it: its default FK name, built from two table names, is 66 bytes. Shortened the dependent table; the changelog says to name such constraints, since a name-only change renames in place. Also bumps the version to 5.2.0, the package-validation baseline to 5.1.0 and SECURITY.md's supported line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GetComplexIndexes() / GetDeclaredComplexIndexes() on IReadOnlyEntityType and GetComplexIndexes() / FindComplexIndex(name) on IReadOnlyModel read every declaration this package holds back as ComplexIndexDeclaration: property-level, entity-level, composite and expression indexes unified, with parts as property paths, IsUnique, Filter, the explicit Name and the provider options of entity-level declarations. The PostgreSQL package adds the same surface for exclusion constraints (ExclusionConstraintDeclaration, GetExclusionConstraints, FindExclusionConstraint), which makes ExclusionPartDefinition and NpgsqlExclusionAnnotations public. Both work on the mutable model inside OnModelCreating, which is where an application checks a convention like "every unique index and exclusion constraint on a withdrawable aggregate is filtered to live rows" — the obligation nothing enforced, and one that could only be checked for the index half while the exclusion side was internal. The differs now build their descriptors from these readers and resolve columns on top, so there is one parser: a read model that disagreed with the differ would check something other than what the migration enforces. The JSON normalization moved to AnnotationValues for the same reason. Declarations are reported unresolved — Name is null for a default-named one, so FindComplexIndex matches explicit names only — because resolution needs the relational model and, for JSON members and templates, the satellite. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a converter
Filters were spliced verbatim while expression parts already resolved
{Property.Path} templates, so a filter had to repeat a column name that
HasColumnName decides elsewhere. ResolveFilter now substitutes
placeholders — quoted columns, or JSON extractions via
ResolveUnmappedPart — while descriptors are built, for indexes in the
core and for exclusion constraints in the Npgsql differ. The resolved
text rides on the operation, so the stock generator renders it with no
runtime wiring, and both sides of the diff compare on it, so a
placeholder filter never churns.
Filters are pre-existing SQL, so the rule is narrower than for
templates: only a brace pair holding a dotted identifier path outside a
single-quoted literal is a placeholder. '{urgent}' is an array literal,
'{"a": 1}' a JSON document, '{{1,2},{3,4}}' a two-dimensional array —
all untouched, which is also why there is no {{ escape. An
unresolvable placeholder throws; outside a literal braces are never
valid SQL.
Template resolution moves from the Npgsql differ into the core over a
new QuoteIdentifier virtual (ANSI by default, brackets in the SQL Server
satellite), which is what lets every provider resolve filters. The path
walk is shared as ResolveProperty and unwraps one member of a
converter-mapped value object: Email.Value resolves to the Email column
only when the property has a converter and the member's type equals the
converter's provider type. Without that check CreatedAt.Year would
silently index the whole column; it still fails, and says so.
Reverting the type check and the literal tracking makes the mismatched-
member, unconverted-member and literals-stay-verbatim tests fail (and
the four tests sharing the literal context with them).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AddComplexIndexFilter(predicate, where) on IMutableEntityType 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; it is ComplexIndexStorage.AddOrReplace retyped to IMutableEntityType, so it carries the fluent API's identity and name rules. The PostgreSQL package mirrors the filter half with AddExclusionConstraintFilter. An unfiltered declaration gets the predicate, a filtered one (existing) AND (predicate), and one that already carries it — as the whole filter or as the exact conjunct this appends — is left alone, so the calls are safe to repeat. The policy itself (which types are withdrawable, the opt-out for "unique across withdrawn rows too") stays with the application: it passes a where predicate. The calls amend what is declared at the time of the call, so they belong at the end of OnModelCreating, never in a model-finalizing convention, whose convention-source write could not overwrite the explicit annotation. Removing the idempotence rule makes the double-application test and the Conjoin rules test fail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every filter: string has a lambda form — HasComplexIndex(x => x.Email,
x => x.RevokedAt == null), HasComplexCompositeIndex, HasExpressionIndex,
HasExclusionConstraint — and the builders take HasFilter(x => …)
(HasFilter<TEntity> on the non-generic index builders, which have no
entity type to infer). AddComplexIndexFilter and
AddExclusionConstraintFilter get typed forms too.
NpgsqlLinqIndexTranslator.TranslatePredicate turns the predicate into a
placeholder filter at the declaration, so nothing downstream knows the
filter was typed: the differ resolves {RevokedAt} like any placeholder,
the resolved SQL is baked into the migration, and the model does not
churn. The subset is boolean structure over the expression translator's
operands: ==/!= (IS [NOT] NULL against null), <, <=, >, >=, &&, ||, !,
boolean properties, captured booleans.
Two refusals are deliberate and throw at the declaration. Enums: how one
is stored depends on the property's value conversion, which the
translator cannot see, so Status == Status.Active would compare a text
column against 0 and fail at apply time. And literals without a portable
SQL spelling — DateTime, Guid — which the IFormattable arm used to
render as bare text in typed expression indexes as well; that arm now
accepts numbers only.
Removing the enum guard and the literal tightening makes the two
refusal tests fail.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
The five features AuditOffice's review of its own workarounds asked for, one commit per concern, in the order they pay off.
GetMaxIdentifierLength(): 63 bytes on PostgreSQL, 128 chars on SQL Server) is rejected atmigrations add. PostgreSQL would truncate it with a NOTICE and apply cleanly, leaving the object under a name nothing reports. Default names are checked too; the package's own temporal-FK test model was the first to hit it (66 bytes).GetComplexIndexes()/FindComplexIndex(name)in core,GetExclusionConstraints()/FindExclusionConstraint(name)in PostgreSQL, on the mutable model insideOnModelCreatingas well. The differs now build their descriptors from these readers, so there is one parser.filter: "{RevokedAt} IS NULL"resolves at design time into the migration (no runtime seam), only for a dotted path in braces outside a single-quoted literal, so'{urgent}'and'{"a": 1}'stay verbatim.x => x.Email.Valueon a converter-mapped value object resolves to its column when the member's type is the provider type;CreatedAt.Yearstill fails. Template resolution moved into core over aQuoteIdentifierseam (SQL Server brackets).AddComplexIndexFilter/AddExclusionConstraintFilterAND a predicate onto selected declarations, idempotently;AddComplexIndexadds one with the fluent API's identity rules. The withdrawable policy stays with the application viawhere.filter:has a lambda form and the builders takeHasFilter(x => …). Enums andDateTime/Guidliterals are refused at the declaration; the latter is a behaviour change for typed expression indexes too, where they used to render as bare text.Verification
dotnet pack -c Releasewith package validation against the 5.1.0 baseline, clean.test/consumer-smoke-test.shagainst that packed feed: passed for both satellites.Deliberately left out
Resolved-name lookup (
FindComplexIndexmatches explicit names only — default names need the differ),AddExclusionConstraintonIMutableEntityType(needs the definition type public), typed filters on SQL Server (the translator is Npgsql-internal; the quoting seam now exists), and a temporal-constraint read model.🤖 Generated with Claude Code