Skip to content

Release 5.4.0 - #32

Merged
CaffeinatedCoder merged 3 commits into
mainfrom
release/5.4.0
Sep 22, 2026
Merged

CaffeinatedCoder merged 3 commits into
mainfrom
release/5.4.0

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Fixes to what 5.x already ships, found while testing the packages against EF Core 11 rc.1: JSON-member indexes that no query could use, and constraint names that only failed — or quietly vanished — when the migration was applied. The previously planned 5.4.0 scope (generic descriptor differ, delete constraint) moves to 5.5.0.

Changes

JSON-member indexes are written the way Npgsql's queries read the member (0b5a8d1, 127ff6d)

  • PostgreSQL uses an expression index only for a query whose expression matches. Every member was rendered as "doc" -> 'A' ->> 'B' text with no cast, which matched only a top-level string. Indexes on nested members (#>> '{A,B}' in the query) or typed members (CAST(… AS integer)) applied, enforced uniqueness, and served no query.
  • Members now render exactly as NpgsqlQuerySqlGenerator renders them (identical in Npgsql 10 and 11): ->> / #>>, a cast to the store type unless it is a string mapping, decode(…, 'base64') for bytea, jsonb for a primitive collection or a json/jsonb member. This applies to index parts, typed expression indexes and index filters; exclusion constraint filters keep their rendering.
  • Rollout: parts are resolved at diff time on both sides, so a rendering change alone never produces a migration. Every declaration now writes CustomIndex:RenderingVersion onto the model; a snapshot without it is rendered by the old rules, so each affected index is dropped and re-created once, under its existing name.
  • Default names come from the path, never from the rendered SQL, so no index is renamed.
  • ResolvedIndexPart equality ignores the new internal metadata, so identical SQL never rebuilds.
  • Date/time members stay text, because their casts are not IMMUTABLE. A non-unique index that starts with one is rejected at migrations add; unique ones are allowed.

Names checked across kinds on PostgreSQL (3c8e11b, roadmap D0)

  • Constraint names are checked per table (42710) and index names per schema (42P07). The index behind every PK, unique, exclusion and temporal constraint counts as an index name.
  • Against an exclusion constraint a clash did not fail at all. Its DROP CONSTRAINT IF EXISTS removed a same-named temporal constraint. Confirmed on PostgreSQL 18: the migration applied clean and only the EXCLUDE remained.
  • Only collisions involving this package's declarations are reported, on the target model only.

Consumer-visible effects

  • After upgrading, the first migrations add rebuilds the affected JSON-member indexes. Until that migration exists, has-pending-model-changes reports changes and Migrate() raises EF Core's pending-model-changes error.
  • On large tables, declare IsCreatedConcurrently() before generating that migration. The custom generator renders CONCURRENTLY outside a transaction.
  • Snapshots gain one HasAnnotation("CustomIndex:RenderingVersion", 2) line, which causes no DDL on its own.
  • New rejections at migrations add: a non-unique index leading with a date/time JSON member, and name collisions.

Verification

  • Tests: 328 pass on net10.0, including 9 against PostgreSQL 18.
  • Guards: every new check was reverted once, and its tests failed for the intended reason.
    • Rendering: reverting it fails 11 tests; the live one reports ux_ig_tenants_city is not used: Seq Scan.
    • Rollout marker: 3 tests. Name tokens: 2. Part equality: 2. Exclusion pinning: 3. Date/time check: 2. Name registry: 8. jsonb members: 1.
  • Alignment: rendering is asserted against EF's own ToQueryString() for every member type, so a future Npgsql translation change fails a test.
  • Rollout: checked on a real Roslyn-compiled snapshot, both with and without the marker.
  • On a live database: PostgreSQL 18 EXPLAIN with enable_seqscan = off shows EF's queries using nested, int, decimal, bool and enum member indexes.
  • Delivery: the consumer smoke test passes against the packed 5.4.0, and dotnet pack -c Release validates cleanly against the 5.3.0 baseline.
  • Review: migration-safety-review was run over the branch. It found the jsonb-member case (127ff6d) and confirmed that the stock generator still fails loudly via the sentinel, that the marker alone yields zero operations, and that real, character(1), smallint, interval, timestamptz and time line up with the query translation.

Not in this PR

  • SQLite index names are database-wide, but the core differ checks them per table. This is tracked separately.
  • ROADMAP.md corrections: the sixth MigrationsModelDiffer ctor parameter is EF 12, not 11; the [Obsolete] temporal constants item is moot (the class is internal and the generator still reads them for pre-5.0.2 migrations); and the 5.4.0/5.5.0 split.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 3 commits September 22, 2026 20:54
PostgreSQL uses an expression index only for a query whose expression
matches it. Every JSON member was rendered as "doc" -> 'A' ->> 'B' text
with no cast, which matched Npgsql's query translation only for a
top-level string: indexes on nested members (#>> '{A,B}' in the query)
or typed members (CAST(... AS integer)) applied, enforced uniqueness and
served no query. Members now render exactly as NpgsqlQuerySqlGenerator
renders them (identical in Npgsql 10 and 11), in index parts, typed
expression indexes and index filters.

Parts are resolved at diff time on both sides, so a rendering change
alone never produces a migration. Every declaration now writes
CustomIndex:RenderingVersion onto the model; a snapshot without it is
rendered by the old rules, which turns the fix into one drop-and-create
per affected index. Default names come from the path (NameToken), and
ResolvedIndexPart equality ignores the derived metadata, so nothing is
renamed or rebuilt without a reason. Exclusion constraint filters keep
the old rendering.

Date and time members stay text (their casts are not IMMUTABLE); a
non-unique index leading with one is rejected at migrations add.

Guards: rendering asserted against EF's own ToQueryString(); rollout on
a real compiled snapshot; EXPLAIN on PostgreSQL 18 with seqscan off.
Reverting each of the six parts of the fix turns its tests red.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
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. Each kind was checked
against its own kind on one table at most, so 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 elsewhere in the
schema scaffolded cleanly and failed at apply time (42710, 42P07).

Against an exclusion constraint it did not fail at all: the exclusion
ADD is preceded by DROP CONSTRAINT IF EXISTS, so the same-named
temporal constraint was dropped. Confirmed on PostgreSQL 18: the
migration applied clean and only the EXCLUDE constraint remained.

ValidateNamesAcrossKinds runs last over the target model's complex
indexes, exclusion, temporal and temporal FK constraints plus EF's own
indexes, keys, foreign keys and check constraints, and reports a
collision when one party is this package's. The snapshot side stays
diffable. Removing the check fails the 8 collision tests; the 4 tests
for legitimate reuse pass either way, as they guard against the check
growing too strict.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Found by the migration-safety review of this release. A json/jsonb
scalar inside a ToJson document (a JsonDocument, say) rendered as
CAST("doc" ->> 'Raw' AS jsonb), while Npgsql's query translation reads
it as "doc" -> 'Raw' (its NpgsqlJsonTypeMapping case, checked before the
string case). The index would not match the query, and the text round
trip fails CREATE INDEX on any document holding a JSON string there.
Reverting the check makes the new test fail with the CAST form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CaffeinatedCoder
CaffeinatedCoder merged commit 19c53bb into main Sep 22, 2026
9 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the release/5.4.0 branch September 22, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant