Repository navigation
Epic: 10.3.0 #408
Description
Goal
10.3.0 is the third release on the 10.x LTS line, shipped from release/10.x. It delivers the first new SQL translations as a small, proven expansion: scalar members that run in memory today and that composed SQL can answer exactly as memory does. Decided 2026-10-25 after an outside review of the translation reasons (#402). 10.2.0 (#358) stays corrective and ships first; its scope doesn't change.
Widened 2026-10-25, before work started, with what the additions already depend on: the translation-support inventory, the main fixes behind the two ports, and two small internal fixes and a docs item in the same area. No new kind of feature was added.
Prerequisites
The additions can't meet their "done when" without these, so each addition is linked as blocked by them.
- 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), on
main: the translation-support inventory,translation-support.jsonand its probe. Stays under the oracle epic (Epic: Systematic bug finding with generated oracles #36), since it ships with 11.0.0 onmain. Done. - 10.x: translation-support inventory, every public query member classified and probed (port of #384) #430: its port to
release/10.x(PR test: translation-support inventory on 10.x, every public query member classified and probed (port of #384, #430) #431). It registers RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385's cells against 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396 until that lands. Done.
In scope
Additions (in priority order)
- Translate Length on Int32Range and Int64Range #405:
LengthonInt32RangeandInt64Range. Compact scalar SQL with explicit widening, plus thenumericpath and NULL forInt64Range's too-large counts. Done onmain(PR feat: translate Length on Int32Range and Int64Range (#405) #437), and onrelease/10.xas 10.x: translate Length on Int32Range and Int64Range (port of #405) #438 (PR feat: translate Length on Int32Range and Int64Range, on 10.x (port of #405, #438) #439). - Translate Clamp on Int32Range, Int64Range and DecimalRange #406:
ClamponInt32Range,Int64RangeandDecimalRange.LEAST/GREATESTover the existing bound semantics, with no subquery. Done onmain(PR feat: translate Clamp on Int32Range, Int64Range and DecimalRange (#406) #440), and onrelease/10.xas 10.x: translate Clamp on Int32Range, Int64Range and DecimalRange (port of #406) #441 (PR feat: translate Clamp on Int32Range, Int64Range and DecimalRange, on 10.x (port of #406, #441) #442). - Translate the calendar factories when they read a column #191: the calendar factories over a column (
Month,Quarter,Year,IsoWeek). Measured on PostgreSQL 16 and 18.to_daterolls week 53 over, so it isn't used.make_dateaccepts the year 10000, so the year has a guard.[)overdate_trunc(infinity)is empty, so the range is built from memory's first and last periods instead. TheDateRangeforms run as SQL. Done onmain(PR feat: translate the DateRange calendar factories over a column (#191) #447), and onrelease/10.xas 10.x: translate the DateRange calendar factories over a column (port of #191) #448 (PR feat: translate the DateRange calendar factories over a column (10.x, port of #191) #449). TheLocalDateRangeandYearMonthRangeforms stay in memory, because aLocalDateat-infinityand the years before 4713 BC have no PostgreSQL date.
Changed answers
- 10.x: value set Union and Remove over a NULL operand project a value instead of NULL (port of #360) #395, port of 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: a value set
Union/Removeover NULL in a projection. 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 is fixed onmainin PR fix: value set Union and Remove over a NULL operand are NULL in a query (#360) #443 (merged;CASE WHEN a IS NULL OR b IS NULL THEN NULL ELSE f(a, b) END). Done onrelease/10.x: PR fix: value set Union and Remove over a NULL operand are NULL in a query, on 10.x (port of #360, #395) #445, merged. - 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396, port of 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. RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385 refuses the call onmainin PR fix: RangeLowerBoundComparer.Compare runs in memory only in a query (#385) #444 (merged): anyIComparerover a range or set,RangeSet.LowerBoundComparerandComparer<T>.Defaultincluded, runs in memory only in a query, since a parameter-typed comparer can't be told apart. Done onrelease/10.x: PR fix: RangeLowerBoundComparer.Compare runs in memory only in a query, on 10.x (port of #385, #396) #446, merged. - A comparer of ranges or sets declared over IRange<T>, the non-generic IComparer or a value set still compares in the range order in SQL #452, found by the pre-release sweep: 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396's rewrite decided by
Compare's declared parameter type, so a comparer declared overIRange<T>, the non-genericICompareror a value set still compared in the range order in SQL. Fixed by deciding on the operands: PR fix: keep a comparer out of SQL by what it compares, not its declared type (#452) #453 (main) and fix: keep a comparer out of SQL by what it compares, not its declared type (#452, 10.x) #454 (release/10.x). It ships under 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396's row, which now links A comparer of ranges or sets declared over IRange<T>, the non-generic IComparer or a value set still compares in the range order in SQL #452. Done: both merged. - A Compare over ranges in a query runs another method, or runs in SQL, when it isn't IComparer's own #464, found by an outside pre-tag review of 15085f9: 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396's rewrite called
IComparer<T>.Compareinstead of the method the code called, so a publicComparebeside a different explicit implementation answered as the explicit one. It also skipped a staticCompareand one on a class that isn't a comparer, which still ran in SQL in the range order. Fixed by running the call as written: PR fix: run a Compare over ranges in a query as the method the code called (#464) #466 (main) and fix: run a Compare over ranges in a query as the method the code called (#464, 10.x) #468 (release/10.x). It ships under 10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396's row, which now links A Compare over ranges in a query runs another method, or runs in SQL, when it isn't IComparer's own #464. Done: both merged.
Both are documented as known differences in 10.2.0 (#397). Each removes its entry. Since the widening, #360 and #385 are committed work on main for this release (both stay in milestone 11.0.0, where they ship on main), and each blocks its port. Both are done: #360 and #385 merged into main (PRs #443, #444), and both ports into release/10.x (PRs #445, #446), so both ship in 10.3.0. The section "Known differences in 10.2.0" stays as a heading that says 10.3.0 fixes both, since 10.2.0's changelog links it.
Internal (no answer changes)
- Sweep the EF translators for name-only dispatch and default operand classification #295: the sweep for translator arms dispatched on a name, and the fix to
Operand()inTranslateRangeSetCall. It goes ahead of the additions, so their new arms are written under it. Done onmain(PR test: sweep the EF translators for operands mapped as a type their overload does not declare (#295) #432), and onrelease/10.xas 10.x: map each translator operand from its declared type, and sweep for it (port of #295) #433 (PR test: map each translator operand from its declared type, and sweep for it, on 10.x (port of #295, #433) #435). - 10.x:
RangeSetContains.Equivalenthands any other one-argumentContainsto the value overload (port of #320) #326, port of main:RangeSetContains.Equivalenthands any other one-argumentContainsonRangeSetto the value overload #320:RangeSetContains.Equivalentsends any other one-argumentContainsto the value overload. Done: main:RangeSetContains.Equivalenthands any other one-argumentContainsonRangeSetto the value overload #320 onmain(PR fix: RangeSetContains.Equivalent refuses a Contains overload it has no static form for (#320) #434), 10.x:RangeSetContains.Equivalenthands any other one-argumentContainsto the value overload (port of #320) #326 onrelease/10.x(PR fix: RangeSetContains.Equivalent refuses a Contains overload it has no static form for, on 10.x (port of #320, #326) #436). - tests: a second seed for the calendar factories' refusals, and a join case for E76 #451, from the Translate the calendar factories when they read a column #191 review: a second seed for
CalendarFactoryParityTests' refusal half, and a join test for E76. PR test: a second calendar seed for the refusals, and a join on a period (#451) #456 (main, stacked on docs: say exactly where the calendar factories' limits come from (#450) #455) and test: a second calendar seed for the refusals, and a join on a period (#451, 10.x) #459 (release/10.x, stacked on docs: say exactly where the calendar factories' limits come from (#450, 10.x) #458). Done: both merged. - CI: pin the .NET 10 SDK to 10.0.402 while aka.ms links resolve to a host that answers 409 #461: pin CI's .NET 10 SDK to 10.0.402. Resolving "latest 10.0" from
global.jsonwent through an aka.ms link that redirects to a host answering 409, so every 10.x job failed at "Setup .NET".release/10.x'sdotnet.ymlandrelease.yml(ci: pin the .NET 10 SDK to 10.0.402 (#461, 10.x) #462), andmain's docs job (ci: pin the docs job's .NET 10 SDK to 10.0.402 (#461) #463), now ask for the exact version. Done: both merged. Remove the pin once aka.ms works again;release.ymlis first exercised by thev10.3.0tag.
Docs (docs only, no package change, so they don't add risk to the release)
- Docs site: publish docs/ to GitHub Pages, one build per line, the stable line by default #417: the docs site on GitHub Pages, one build per line, with 10.x as the default. Done.
- Walkthrough: one booking app from request to response, as a sample that CI builds and runs #418 / 10.x: the booking walkthrough and its sample (port of #418) #423: the booking walkthrough, a sample that CI builds and runs, plus
docs/walkthrough.md. Done, with Point the repo homepage, README and NuGet pages at the docs site and the walkthrough #424 (homepage, READMEs, NuGet project URL). - Document that range mappings resolve by store type name #56: say that range mappings resolve by store type name, and why value sets don't. PR docs: say what a store type name maps to on its own (#56) #457 (
main) and docs: say what a store type name maps to on its own (#56, 10.x) #460 (release/10.x). Also says that withUseValueRangesNodaTime, Npgsql's NodaTime plugin resolvesdaterange,tsrangeandtstzrangefirst. Done: both merged. - docs: calendar factory rows in What runs where — PostgreSQL's first date, the extremes with the conversions off, Quarter's error text #450, from the Translate the calendar factories when they read a column #191 review: the calendar factory rows in What runs where (PostgreSQL's first date, the extremes with the conversions off, the error text). PR docs: say exactly where the calendar factories' limits come from (#450) #455 (
main) and docs: say exactly where the calendar factories' limits come from (#450, 10.x) #458 (release/10.x). Done: both merged. - Booking sample answers 500 for a null or missing stay #465, from the same outside review: the booking sample answered 500 (
UnreachableException) for a null or missing stay.CheckStaynow handlesnull, and the walkthrough says why it arrives. PR fix: answer 400 for a null or missing stay in the booking sample (#465) #467 (main) and fix: answer 400 for a null or missing stay in the booking sample (#465, 10.x) #469 (release/10.x). Done: both merged.
All of them land on main first and are cherry-picked into release/10.x, like the additions. None holds up the tag: one that isn't finished in time moves to the next minor.
Parity: what every translation has to show
A translation moves work from memory to the server, so where it runs changes. Its answers mustn't. Each addition is checked against memory over the loaded rows on a live server, for:
- shapes: empty, finite, unbounded on either side, and
(,); - domain extremes: the type's minimum and maximum, and for temporal types the values that stand for
infinity; - nullable operands: a nullable column, the optional side of an outer join, a nullable argument. Rule (c) applies: a value-producing member over NULL is NULL;
- composition:
Where, negatedWhere,OrderBy, inside a composed predicate, and in a finalSelect, which ran in memory before, so its parity is a requirement too and not a given; - declared nullability: matches the function's real strictness (
DeclaredNullabilityTests).
On main, the composition sweeps (OuterJoinProbeTests, NullableOperandPairProbeTests, QuantifierProbeTests) and the translation-support probe (#384) cover a new member if their discovery reaches it. Check that they do, rather than assume it. release/10.x has none of the composition sweeps, so each cherry-pick carries its own live test of the list above, as #387 and #388 did in 10.2.0. It does get the translation-support probe, through #430.
It isn't done until translation-support.json classifies it server on both lines, the translation-support probe passes, and docs/efcore.md#what-runs-where moves its row. A cell that can't match memory keeps the member in memory for that type.
Sequence
10.2.0 tags (Release 10.2.0: date the changelogs, tag v10.2.0, close the 10.x-only issues, carry the sections to main #365).Done.Open 10.3.0 onDone.release/10.x(Open 10.3.0 on release/10.x #426).Translation-support inventory: every public query member classified as server, client-only or planned, and probed against EF Core #384 merges intoDone 2026-10-25 (10.x: map each translator operand from its declared type, and sweep for it (port of #295) #433 ported Sweep the EF translators for name-only dispatch and default operand classification #295).main(PR test: translation-support inventory — every public query member classified and probed against EF Core (#384) #386), then 10.x: translation-support inventory, every public query member classified and probed (port of #384) #430 ports it. Sweep the EF translators for name-only dispatch and default operand classification #295 and main:RangeSetContains.Equivalenthands any other one-argumentContainsonRangeSetto the value overload #320 land onmainaround the same time, and 10.x:RangeSetContains.Equivalenthands any other one-argumentContainsto the value overload (port of #320) #326 follows.- Each addition merges into
mainfirst, so the 11 line has it before 10.x does (CONTRIBUTING.md#branches-and-releases). Then it's cherry-picked with-xinto a PR againstrelease/10.x. The same goes for 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, through 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. Tag.Done 2026-10-25:v10.3.0on 2230e73, all four packages on nuget.org (Release 10.3.0: date the changelogs, tag v10.3.0, carry the sections to main #470). The carry tomainis chore: carry the 10.3.0 sections to main (#470) #474, and 10.4.0 is opened in chore: open 10.4.0 on release/10.x (#473) #475 (epic Epic: 10.4.0 #472).
Not in scope
- Translate enumeration and the bridge conversions — a batch after 10.3.0 #407: enumeration, the bridge conversions and value set
Intersect/Except, a separate batch.Intersect/Exceptmust also be right over a non-canonical server-sideUnion. Translate enumeration of a multirange column: Count, Any, All, SelectMany over a RangeSet (unnest) #94, Translate element-wise predicates over a value set column: Any, All, SelectMany (unnest) #95 and Aggregate a column into a value set in a GroupBy projection (array_agg) #96 (unnest,array_agg) belong with it. - Opt-in strict finite factory: refuse start > end as written, translated to the bare PostgreSQL constructor #429, the strict finite factory: an addition with a translation, but its public API (the name, whether it goes on
IRangeFactory, the exception) isn't settled yet. A candidate for the next minor. - Value set
Add: inserting in canonical order would sort by the database collation. Shift/Expand,Lengthon the other range types, andClampon the temporal and float types: each has differences from memory (docs: the in-memory reasons overstate what PostgreSQL lacks, and the Shift/Expand one is stale since #224 #402) that need their own design.- CI and oracle work that changes no package (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, Type the plugin's NULL constants in its type mappings instead of matching EF Core's empty-root shape #327, the servicing and quirk registries): not tied to a release.
- Anything
breaking.
Release
Tag v10.3.0 on release/10.x. The root changelog section has ### Added for #405, #406 and #191, and ### Changed answers for #395 and #396 (both rows are in, linking docs/migration.md#changed-answers-in-1030). When 10.3.0 is carried back to main, add ## Changed answers in 10.3.0 to main's docs/migration.md (the rows link blob/main/docs/migration.md#changed-answers-in-1030), pointing at the two sections under Migration from v10.x, and say there that the last two of its list also shipped in 10.3.0.
Activity
10 remaining items
feat: translate Clamp on Int32Range, Int64Range and DecimalRange (#406)
CaffeinatedCoder commentedon Oct 25, 2026
State, 2026-10-25: #405 (
Length) and #406 (Clamp) run as SQL on both lines (ports #438 and #441). Open: #191 (calendar factories over a column, measurements first) and #56 (docs). Still blocked by theirmainfixes: #395 (needs #360) and #396 (needs #385).CaffeinatedCoder commentedon Oct 25, 2026
State, 2026-10-25: every remaining item has a PR on both lines. #452 (silent, from the pre-release sweep): #453 →
main, #454 →release/10.x. #450: #455 / #458. #451: #456 / #459, each stacked on #450's PR. #56: #457 / #460. Merge order: #453 and #454 first, since #452 is the only one that changes an answer; then #455 before #456 and #458 before #459. Each issue is closed by hand once its 10.x PR merges. The seed prover from the pre-release review was still running when these were opened.CaffeinatedCoder commentedon Oct 25, 2026
State, 2026-10-25 21:09 UTC: every sub-issue is closed. Merged on both lines, in order: #452 (#453, #454), #450 (#455, #458), #451 (#456, #459) and #56 (#457, #460). Each 10.x issue was closed by hand after its 10.x PR merged. #461 pinned CI's .NET 10 SDK to 10.0.402 (#462 on
release/10.x, #463 onmain): Microsoft's aka.ms link for "latest 10.0" redirects to a host answering 409. GitHub's ownsubmit-nugetdependency submission still fails on that download; the pin can't reach it, and it isn't a required check. Left for the tag: the dates in the five changelog headings, and the seed prover result from the pre-release review session.release.ymlfirst runs with the pin atv10.3.0.CaffeinatedCoder commentedon Oct 25, 2026
10.3.0 shipped on 2026-10-25. The
v10.3.0tag is on 2230e73, release run 40236600453 is green, all four packages are on nuget.org, and the GitHub release is Latest. The run was also the first use of the 10.0.402 SDK pin inrelease.yml(#461), which held.What shipped
LengthonInt32Range/Int64Range(Translate Length on Int32Range and Int64Range #405, port 10.x: translate Length on Int32Range and Int64Range (port of #405) #438),ClamponInt32Range/Int64Range/DecimalRange(Translate Clamp on Int32Range, Int64Range and DecimalRange #406, port 10.x: translate Clamp on Int32Range, Int64Range and DecimalRange (port of #406) #441), and theDateRangecalendar factories over a column (Translate the calendar factories when they read a column #191, port 10.x: translate the DateRange calendar factories over a column (port of #191) #448). Each one is checked against memory on PostgreSQL 16 and 18.Union/Removeover NULL is NULL in a query (10.x: value set Union and Remove over a NULL operand project a value instead of NULL (port of #360) #395, port of 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). AnyCompare(x, y)over ranges or sets runs in memory only in a query, as the method the code called (10.x: RangeLowerBoundComparer.Compare in a query runs in the range order (port of #385) #396, port of RangeLowerBoundComparer.Compare runs as SQL in the range order: EF Core rewrites any Compare(a, b) to the natural comparison #385, widened by A comparer of ranges or sets declared over IRange<T>, the non-generic IComparer or a value set still compares in the range order in SQL #452 and A Compare over ranges in a query runs another method, or runs in SQL, when it isn't IComparer's own #464).RangeSetContains.Equivalent(10.x:RangeSetContains.Equivalenthands any other one-argumentContainsto the value overload (port of #320) #326), the translation-support inventory (10.x: translation-support inventory, every public query member classified and probed (port of #384) #430), the calendar seed and join test (tests: a second seed for the calendar factories' refusals, and a join case for E76 #451), and the CI SDK pin (CI: pin the .NET 10 SDK to 10.0.402 while aka.ms links resolve to a host that answers 409 #461).Pre-tag checks
Comparerun in SQL (silent). Booking sample answers 500 for a null or missing stay #465 was the sample answering 500 for a null stay.dotnet run tools/oracles.cs seedscaught all 125 seed/oracle pairs on the release tree, and every CI lane was green on 2230e73 (Release 10.3.0: date the changelogs, tag v10.3.0, carry the sections to main #470).After the tag
mainin chore: carry the 10.3.0 sections to main (#470) #474: the five changelog sections,## Changed answers in 10.3.0, and the baseline, SECURITY.md row and sample moved to 10.3.0. That closed Release 10.3.0: date the changelogs, tag v10.3.0, carry the sections to main #470.release/10.xin chore: open 10.4.0 on release/10.x (#473) #475 (Open 10.4.0 on release/10.x #473), under epic Epic: 10.4.0 #472 and milestone 10.4.0. Its scope isn't decided yet, and the candidates are Translate enumeration and the bridge conversions — a batch after 10.3.0 #407 and Opt-in strict finite factory: refuse start > end as written, translated to the bare PostgreSQL constructor #429.