Repository navigation
Epic: 10.2.0 #358
Description
Goal
10.2.0 is the second release on the 10.x LTS line, shipped from release/10.x. It is a minor because it carries silent fixes, which ship only in a minor that opens with ### Changed answers (CONTRIBUTING.md, "A fix that changes an answer"). Both came from a consumer's differential probe against 10.1.0 (2026-10-23), which ran each EF Core query and the same LINQ over the loaded rows. The query and memory selected different rows in two cases: when a range bound is compared with null, and when a predicate runs over a NULL column.
In scope
Changed answers
- 10.x: LowerBound() and UpperBound() in a query: == null becomes WHERE FALSE and ?? loses its COALESCE (port of #349) #354, port of LowerBound() and UpperBound() in a query: == null becomes WHERE FALSE and ?? max loses its COALESCE, because lower() and upper() are declared NULL only for a NULL argument #349 (severity/high):
LowerBound()andUpperBound()compared withnull, read throughHasValueor coalesced with??in a query.lower()andupper()were declared NULL only for a NULL argument, so on a required column== nullbecameWHERE FALSEand??lost itsCOALESCE. This applies to every range type andRangeSet. The fix is one flag, onmainin PR fix: declare lower() and upper() NULL for a non-null range #353, which is stacked on PR test: register #349 and #350 as ExecutedQueryTests candidates #352; the port cherry-picks both. - 10.x: RangeIntersectAgg returns empty over a sequence holding a null range, where range_intersect_agg skips the NULL; RangeAgg throws (port of #361) #363, port of RangeIntersectAgg returns empty over a sequence holding a null range, where range_intersect_agg skips the NULL; RangeAgg throws #361 (severity/low):
RangeIntersectAggreturnsemptyover a sequence holding a null range, whererange_intersect_aggskips the NULL, andRangeAggthrows. The docs promise PostgreSQL's aggregates, so both skip null ranges. The fix is in memory only and small; RangeIntersectAgg returns empty over a sequence holding a null range, where range_intersect_agg skips the NULL; RangeAgg throws #361 lands onmainfirst. - 10.x: a predicate over a null range, RangeSet or value set is false in memory and in SQL (port of the predicate half of #350, #355 and #356) #357, port of the predicate half of A null range answers some extensions in memory and throws on others, and the answers it gives disagree with PostgreSQL on a NULL column #350 with A predicate over a NULL range, RangeSet or value set column is NULL in SQL, so its negation drops the row EF Core's own translations keep #355 and RangeSet.From accepts a null element: the set holds null, IsEmpty() is false and Contains throws #356 (severity/medium): a Boolean predicate over a null range,
RangeSetor value set isfalsein memory and in SQL, and its negationtrue. This is rule (c), decided on A null range answers some extensions in memory and throws on others, and the answers it gives disagree with PostgreSQL on a NULL column #350 on 2026-10-24. With it,RangeSet.Fromrejects a null element. The SQL shapex IS NOT NULL AND <predicate>was measured identical on EF Core 10.0.0 and on 10.0.13 with Npgsql 10.0.3. Required columns and non-null parameters keep today's SQL. Themainwork (A null range answers some extensions in memory and throws on others, and the answers it gives disagree with PostgreSQL on a NULL column #350's predicate part, A predicate over a NULL range, RangeSet or value set column is NULL in SQL, so its negation drops the row EF Core's own translations keep #355, RangeSet.From accepts a null element: the set holds null, IsEmpty() is false and Contains throws #356) is under Epic: 11.0.0 #147 and comes first. - 10.x: Contains(element) on a range or RangeSet is NULL in SQL for a NULL element (port of #381) #387, port of Contains(element) on a range or RangeSet is NULL in SQL for a NULL element: the negation drops the row EF Core keeps, and a projection into bool throws #381 (severity/low), added 2026-10-24 when the open bugs were re-evaluated:
Contains(T)on a range orRangeSetis NULL in SQL for a NULL element (outer join,.Valueon a nullable column). The negation drops the row, aboolprojection throws, andAllselects the NULL rows. The fix finishes rule (c):FalseOverNull(on 10.x since 10.x: a predicate over a null range, RangeSet or value set is false in memory and in SQL (port of the predicate half of #350, #355 and #356) #357) also wraps the element operand. It is SQL only. - 10.x: the range factories over a NULL element build a range instead of answering NULL (port of #382) #388, port of The range factories over a NULL element build a range: CreateFinite gives empty and CreateUnboundedStart/End give (,), where EF Core answers NULL #382 (severity/medium), added the same day:
CreateFinite,CreateUnboundedStartandCreateUnboundedEndover a NULL element buildemptyor(,)instead of NULL, so a positive filter selects a row with no element. The fix puts aCASE … IS NULL THEN NULLaround the factory when an argument can be NULL. It is SQL only and stays in the translator 10.x: a predicate over a null range, RangeSet or value set is false in memory and in SQL (port of the predicate half of #350, #355 and #356) #357 touched.
Docs
- 10.x: docs: ordering by LowerBound() or UpperBound() puts open-ended and empty rows where PostgreSQL puts NULLs (port of #359) #364, port of Docs: ordering by LowerBound() or UpperBound() puts open-ended and empty rows where PostgreSQL puts NULLs, also on a required column #359: say that an ordering by
LowerBound()orUpperBound()puts open-ended and empty rows where PostgreSQL puts NULLs, also on a required column. This follows the documented decision indocs/ranges.md#sort-ranges; nothing in the translation changes. - 10.x: docs: name the two known differences 10.2.0 ships with — value set Union/Remove over NULL (#360) and RangeLowerBoundComparer in a query (#385) #397, added 2026-10-24 after the release review: name the two silent differences 10.2.0 ships with, Value set Union is declared NULL only for a NULL operand, but array_cat isn't strict: a filter and a projection disagree on a NULL column #360 and RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385 (below), each with a workaround and its 10.x port, under
docs/efcore.md#known-differences-in-1020. It'srelease/10.xonly, becausemainfixes both before 11.0.0. - docs: what-runs-where omits Equals, RangeSet.Count, the indexers, Values, Parse/ToString and the NodaTime conversions (found by #384) #399, added 2026-10-25 from the inventory's triage:
docs/efcore.md#what-runs-wherelists every public query member.Equalsruns as=;Values,RangeSet.Countand its indexer,From,Parse, the NodaTime conversions,ToString,GetHashCodeandGetEnumeratorrun in memory. The calendar factories read "not translated" rather than "not translated yet".mainfirst (PR docs: list every public query member in what-runs-where (#399) #400), then the cherry-pick (PR docs: list every public query member in what-runs-where (10.x, #399) #401). - docs: the in-memory reasons overstate what PostgreSQL lacks, and the Shift/Expand one is stale since #224 #402, added 2026-10-25 after an outside review of the translation reasons: every in-memory operation gets its real reason, in
what-runs-where, the READMEs, the guides, AGENTS.md and the XML docs.Shift/Expand'sinfinityreason was stale since Shift/Expand turn a bound that stands for ±infinity into a finite date #224. Several others overstated what PostgreSQL lacks:Length, enumeration, the bridge conversions,Clampand the value setIntersect/Exceptwould take composed SQL, measured on PostgreSQL 16. Only the finalSelectruns on the client. No translation decision changes. PR docs: give each in-memory operation its real reason (#402) #403 is stacked on docs: list every public query member in what-runs-where (#399) #400, and PR docs: give each in-memory operation its real reason (10.x, #402) #404 on docs: list every public query member in what-runs-where (10.x, #399) #401.
Fixed (not silent)
- 10.x: the empty-root rewrite casts EF Core's
_ordcolumn to the range type on EF Core 10.0.2 and later (port of #319) #325, port of main: the empty-root rewrite casts EF Core's_ordcolumn to the range type (NULL::int4range AS _ord) #319: on EF Core 10.0.2 and later, the empty-root rewrite casts EF Core's_ordcolumn to the range type. It's latent and the fix is small:TypeTheNullsskips_ord, asOneRowOfNullsalready does. Moved in from "Not in scope" on 2026-10-24.
Release
- Open 10.2.0 on release/10.x #331 opens 10.2.0 on
release/10.x: the version, the ApiCompat baseline, the SECURITY.md row and the changelog sections. - PR chore: write each package's changelog section into its NuGet release notes (10.x) #348 writes each package's changelog section into its NuGet release notes (the 10.x half of Packages: show the version's changelog section as NuGet release notes #346), so the 10.2.0 page on nuget.org shows its own notes.
- Release 10.2.0: date the changelogs, tag v10.2.0, close the 10.x-only issues, carry the sections to main #365: the checks before the tag, dating the changelogs, the tag, closing the 10.x-only issues by hand, and carrying 10.2.0 to
main.
Sequence
- Open 10.2.0 on release/10.x #331 merges first, so
pr-hygiene.shsees a minor and each fix adds its row under## [10.2.0]. - PR test: register #349 and #350 as ExecutedQueryTests candidates #352 and then PR fix: declare lower() and upper() NULL for a non-null range #353 merge into
main. 10.x: LowerBound() and UpperBound() in a query: == null becomes WHERE FALSE and ?? loses its COALESCE (port of #349) #354 cherry-picks both ontorelease/10.xwith-x. - On
main: RangeSet.From accepts a null element: the set holds null, IsEmpty() is false and Contains throws #356, A predicate over a NULL range, RangeSet or value set column is NULL in SQL, so its negation drops the row EF Core's own translations keep #355 and A null range answers some extensions in memory and throws on others, and the answers it gives disagree with PostgreSQL on a NULL column #350's predicate part. Then 10.x: a predicate over a null range, RangeSet or value set is false in memory and in SQL (port of the predicate half of #350, #355 and #356) #357 ports them. - On
main: RangeIntersectAgg returns empty over a sequence holding a null range, where range_intersect_agg skips the NULL; RangeAgg throws #361 and Docs: ordering by LowerBound() or UpperBound() puts open-ended and empty rows where PostgreSQL puts NULLs, also on a required column #359. Then 10.x: RangeIntersectAgg returns empty over a sequence holding a null range, where range_intersect_agg skips the NULL; RangeAgg throws (port of #361) #363 and 10.x: docs: ordering by LowerBound() or UpperBound() puts open-ended and empty rows where PostgreSQL puts NULLs (port of #359) #364 port them. Neither depends on steps 2 and 3. - On
main: Contains(element) on a range or RangeSet is NULL in SQL for a NULL element: the negation drops the row EF Core keeps, and a projection into bool throws #381 and The range factories over a NULL element build a range: CreateFinite gives empty and CreateUnboundedStart/End give (,), where EF Core answers NULL #382, after PR test: composition sweeps — outer joins, nullable operand pairs, quantifiers, repeated execution, filtered aggregates, declared nullability (#375–#380) #383, which registers their candidates. Then 10.x: Contains(element) on a range or RangeSet is NULL in SQL for a NULL element (port of #381) #387 and 10.x: the range factories over a NULL element build a range instead of answering NULL (port of #382) #388 port them. - On
main: main: the empty-root rewrite casts EF Core's_ordcolumn to the range type (NULL::int4range AS _ord) #319. Then 10.x: the empty-root rewrite casts EF Core's_ordcolumn to the range type on EF Core 10.0.2 and later (port of #319) #325 ports it. It doesn't depend on steps 2–5. - PR chore: write each package's changelog section into its NuGet release notes (10.x) #348 and PR docs: name the two known differences 10.2.0 ships with (10.x, #397) #398 (10.x: docs: name the two known differences 10.2.0 ships with — value set Union/Remove over NULL (#360) and RangeLowerBoundComparer in a query (#385) #397) merge. Then PR docs: list every public query member in what-runs-where (#399) #400 → PR docs: give each in-memory operation its real reason (#402) #403 on
main(docs: what-runs-where omits Equals, RangeSet.Count, the indexers, Values, Parse/ToString and the NodaTime conversions (found by #384) #399, docs: the in-memory reasons overstate what PostgreSQL lacks, and the Shift/Expand one is stale since #224 #402), and PR docs: list every public query member in what-runs-where (10.x, #399) #401 → PR docs: give each in-memory operation its real reason (10.x, #402) #404 on 10.x. - Release 10.2.0: date the changelogs, tag v10.2.0, close the 10.x-only issues, carry the sections to main #365: check every lane, date the five changelogs and tag
v10.2.0(thenugetenvironment approval is the maintainer's), close the 10.x-only issues by hand, and carry 10.2.0 tomain.
Not in scope
- The value-producing half of A null range answers some extensions in memory and throws on others, and the answers it gives disagree with PostgreSQL on a NULL column #350:
Intersect,Gap,Merge,UnionandExceptthrowingArgumentNullExceptionon a null range. It is loud andbreaking, so 11.0.0 only. Four of A null range answers some extensions in memory and throws on others, and the answers it gives disagree with PostgreSQL on a NULL column #350's candidates stay registered on 10.x (10.x: a predicate over a null range, RangeSet or value set is false in memory and in SQL (port of the predicate half of #350, #355 and #356) #357). - Value set Union is declared NULL only for a NULL operand, but array_cat isn't strict: a filter and a projection disagree on a NULL column #360 (value set
UnionandRemovedeclared null-propagating whilearray_catandarray_removearen't strict): low severity, projections only, and its fix has to work withValueSetSqlGuard. Measured on 10.x (EF Core 10.0.0 and 10.0.13) on 2026-10-24. It lands in 11.0.0 first. Its 10.x port is 10.x: value set Union and Remove over a NULL operand project a value instead of NULL (port of #360) #395, scheduled for 10.3.0 (Epic: 10.3.0 #408) if themainfix lands first. Until then 10.x: docs: name the two known differences 10.2.0 ships with — value set Union/Remove over NULL (#360) and RangeLowerBoundComparer in a query (#385) #397 documents it, with the workaroundx == null ? null : x.Union(y). - RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385 (
RangeLowerBoundComparer.Comparein a query runs as the range order): reproduced on EF Core 10.0.0 and 10.0.13 on 2026-10-24. It silently selects different rows. It staysseverity/lowbecause a query reaches it only through an explicitComparecall inside the expression, which no doc shows; the upstream origin (Any method namedCompareis translated as the natural comparison of its arguments, so a comparer's own ordering is silently ignored (StringComparer.OrdinalIgnoreCase, customIComparer<T>) dotnet/efcore#39402) doesn't lower the severity. Deferred because the fix needs a sound choice between refusing and translating, made onmainfirst, in 11.0.0. Its 10.x port is 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396, scheduled for 10.3.0 (Epic: 10.3.0 #408) if themainfix lands first. Until then 10.x: docs: name the two known differences 10.2.0 ships with — value set Union/Remove over NULL (#360) and RangeLowerBoundComparer in a query (#385) #397 documents it, with the workaround of comparing loaded rows. - Context sweep: every translated member in a filter, a negated filter, an ordering, a grouping key, an aggregate and a projection, over required and nullable operands, against LINQ over the loaded rows #362 (the context sweep) and any other new discovery round: oracle work under Epic: Systematic bug finding with generated oracles #36, not release scope (see "Release boundary").
- 10.x:
RangeSetContains.Equivalenthands any other one-argumentContainsto the value overload (port of #320) #326: latent, and itsmainfix (main:RangeSetContains.Equivalenthands any other one-argumentContainsonRangeSetto the value overload #320) is a chore that is still open. A later 10.x release. - Native AOT compiled models (dbcontext optimize --nativeaot) fail: plugin type mappings have no public static Default #209 (Native AOT compiled models): an addition with its own design.
- 10.x CI: run the EF suites weekly against EF Core's staged 10.0 servicing build (darc-pub feed), scheduled from main #312 and 10.x CI: run the net10.0 suites on the .NET 11 runtime (roll-forward), so the hour-24 rows of #300 can fail #324 (10.x CI legs): neither fix here depends on the .NET 11 runtime or on an unpublished servicing build.
- Additions beyond the fixes, and anything
breaking. New SQL translations go to 10.3.0 (Epic: 10.3.0 #408), decided 2026-10-25. That coversLength(Translate Length on Int32Range and Int64Range #405),Clamp(Translate Clamp on Int32Range, Int64Range and DecimalRange #406) and the calendar factories (Translate the calendar factories when they read a column #191), each proven against memory first. 10.2.0 only corrects the explanations (docs: the in-memory reasons overstate what PostgreSQL lacks, and the Shift/Expand one is stale since #224 #402).
Release boundary
Decided 2026-10-24 after the release review: no seventh discovery round before the tag. The six composition sweeps (#375–#380) already cover several independent failure mechanisms. What is left before #365:
evaluate the translation-support inventory (Translation-support inventory: every public query member classified as server, client-only or planned, and probed against EF Core #384, PR test: translation-support inventory — every public query member classified and probed against EF Core (#384) #386) and triage its findings: done 2026-10-25. It ran on EF Core 10.0.0 and 10.0.13 with the same result as onmain. RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385 is its only defect (deferred, 10.x: docs: name the two known differences 10.2.0 ships with — value set Union/Remove over NULL (#360) and RangeLowerBoundComparer in a query (#385) #397). docs: what-runs-where omits Equals, RangeSet.Count, the indexers, Values, Parse/ToString and the NodaTime conversions (found by #384) #399 holds the docs-only gaps and is in 10.2.0 (below);- verify the fixes together on the release commit (Release 10.2.0: date the changelogs, tag v10.2.0, close the 10.x-only issues, carry the sections to main #365, step 1).
A new round becomes worthwhile when it can name a mechanism or a contract that no sweep covers yet. "Perhaps there is another bug" doesn't move this boundary.
Release
Tag v10.2.0 on release/10.x. The root changelog section opens with ### Changed answers, one row per issue (#354, #357, #356, #363, #387, #388), each linking its issue and its entry under docs/migration.md#changed-answers-in-1020. Issues fixed only there are closed by hand (Closes #n acts only on main).
Activity
_ordcolumn to the range type on EF Core 10.0.2 and later (port of #319) #325CaffeinatedCoder commentedon Oct 25, 2026
10.2.0 released 2026-10-25.
v10.2.0(annotated) is on 7dada5d (PR #411); release run 40183166630 is green. All four packages are on nuget.org, and the GitHub release v10.2.0 is Latest._ordcolumn to the range type on EF Core 10.0.2 and later (port of #319) #325. Docs: 10.x: docs: ordering by LowerBound() or UpperBound() puts open-ended and empty rows where PostgreSQL puts NULLs (port of #359) #364, 10.x: docs: name the two known differences 10.2.0 ships with — value set Union/Remove over NULL (#360) and RangeLowerBoundComparer in a query (#385) #397, docs: what-runs-where omits Equals, RangeSet.Count, the indexers, Values, Parse/ToString and the NodaTime conversions (found by #384) #399, docs: the in-memory reasons overstate what PostgreSQL lacks, and the Shift/Expand one is stale since #224 #402. Release: Open 10.2.0 on release/10.x #331, chore: write each package's changelog section into its NuGet release notes (10.x) #348 (NuGet release notes from the changelog), Release 10.2.0: date the changelogs, tag v10.2.0, close the 10.x-only issues, carry the sections to main #365.tools/oracles.cs seeds), and CI green on every merge commit (PostgreSQL 16/18 × EF Core floor/10.0.13, both smoke modes).main: PR chore: carry the 10.2.0 sections to main (#365) #413 (five changelog sections,#changed-answers-in-1020, baseline 10.2.0, SECURITY.md10.2.x).docs/efcore.md#known-differences-in-1020on 10.x): Value set Union is declared NULL only for a NULL operand, but array_cat isn't strict: a filter and a projection disagree on a NULL column #360 and RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385. Their ports 10.x: value set Union and Remove over a NULL operand project a value instead of NULL (port of #360) #395 and 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396 are scheduled for 10.3.0 (Epic: 10.3.0 #408).